Ethernet · Module 20
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.
Module 20 built four components and each one reports a number about itself. None of the four numbers can be combined with any of the others, because the four components have no identifier in common.
| Component | Its number | Keyed on |
|---|---|---|
| Chapter 20.1 §15's generator | lists_complete — five bits | nothing; a run-level tally |
| Chapter 20.2 §15's assertion library | fired_pct_x10 | the checker instance |
| Chapter 20.3 §14's scoreboards | octet_coverage_pct | Chapter 20.3 §7's tag, which is 12 deep and reused |
| Chapter 20.4 §19's coverage model | three percentages | the cell, not the frame |
Four keys, no join. A reviewer cannot ask "of the frames that closed a coverage cell, how many did the scoreboard compare and how many fired a property?" — not because the answer is hard, but because the question does not typecheck. The generator's tally has no frames in it, the scoreboard's tag is recycled every twelve frames, and the coverage model's key is a cell that many frames map to.
This chapter builds the agent, and the agent's first job is not reuse. It is to be the one place a frame identifier exists.
Its second job is the one the module title promises, and the cost of that is arithmetic rather than architecture.
| What changes with the beat width | At 8 octets | At 64 octets |
|---|---|---|
| Chapter 20.4's declared cross | 6 720 cells | 860 160 cells |
| Chapter 20.1 §9's reachable offsets | 2 of 8 | 16 of 64 |
| Chapter 20.1 §17's spill sizes | 182 | 23 |
| Chapter 20.1 §15's runt threshold | 43 | 43 |
Rows one to three move and row four does not, and the agent that "works across the xMII family without rewriting the sequence layer" is the thing that has to know which is which.
1. Scope, and Four Numbers That Cannot Be Joined
Scope: one agent — package, sequence layer, driver, monitor and the join table — parameterised by the beat width, carrying an identifier that survives every component boundary in Module 20.
Not in scope: the UVM base-class mechanics. This chapter is about what an agent has to contain, not about uvm_component and the factory — every structure here is written as synthesisable RTL for the same reason the rest of the track is: a structure you can elaborate is a structure whose cost you can count, and Section 19 counts it.
Start from the failure, because it is specific.
Chapter 20.4 §26 stated it and left it. Four components, four numbers, and each number weakens under exactly the pressure that should strengthen it:
| Number | Weakens when | Chapter |
|---|---|---|
list_coverage_pct | a list is redefined smaller | Chapter 20.1 §15 |
fired_pct_x10 | never-firing checkers are deleted | Chapter 20.2 §15 |
octet_coverage_pct | a field is waived as "transformed" | Chapter 20.3 §14 |
pct_of_reachable | the environment improves | Chapter 20.4 §19 |
Each of those is a known failure and each chapter named its own. What none of them could do is check another one against it — and that is the failure this chapter is about, because the check that matters is a join.
The question a reviewer actually wants to ask:
Of the cells this run closed, what share were closed by a frame the scoreboard compared octet-for-octet and at least one property fired on?
That question has an answer, it is 7.4% for a closed loopback run, and Section 17 derives it. It is not askable today because a coverage cell, a scoreboard entry, a property firing and a generated item are four objects with four keys, and nothing in Module 20 mints a key that all four can carry.
The agent is the place the key lives, and that is the argument for having one.
| The usual argument for an agent | This chapter's argument |
|---|---|
| reuse across projects | true, and Section 10 prices it |
| a clean driver/monitor split | true, and it is not the point |
| a place the frame's identity lives | the point — Sections 3, 4 and 13 |
Row three is the one nobody writes in a verification plan, and it is the only one of the three that makes a number possible that did not exist before.
2. What an Agent Is For, and What It Is Not
An agent is usually described as a container. That description is why agents get built badly, because a container has no invariant and this one does.
The invariant:
Every frame the agent produces or observes has exactly one identifier, and every observation any component in the environment makes about that frame is stamped with it.
Which immediately settles four design questions that are otherwise matters of taste.
| Question | Settled by the invariant |
|---|---|
| where is the uid minted? | at the sequence layer — before the frame exists on any wire |
| how does the monitor learn it? | it cannot be told; it must recover it — Section 4 |
| can the passive agent work? | only if the uid is recoverable from the frame |
| may two agents share a uid space? | no — Section 14's second prohibition |
Row two is the whole difficulty and it is worth stating plainly. A driver knows the uid because it was handed the item. A monitor watching the same wire has octets and nothing else, and in a passive configuration — an agent bound to a link the environment does not drive — there is no item at all. So the identifier must be a function of the frame, not a label attached to it.
And the frame must not be modified to carry it, which rules out the obvious answer.
| Mechanism | Works? | Why not |
|---|---|---|
| a sequence number in the payload | no | it changes the frame under test |
| a reserved EtherType | no | it changes which paths the frame takes |
| a side-channel from driver to monitor | no | a passive agent has no driver |
| a digest of the frame's invariant core | yes | Chapter 20.3 §3 already computes it |
Row four is the answer and it is not free: a digest can collide, and Section 4 is about how often and what to do about it.
What an agent is not:
| Not | Because |
|---|---|
| a place to put the scoreboard | Chapter 20.3's scoreboard is per-path, and an agent is per-interface |
| a place to put coverage | Chapter 20.4's cross spans two interfaces |
| a place to put the checks | Chapter 20.2 §11's harness binds to the DUT, not to the agent |
| width-independent | Sections 6, 8 and 12 — three of Module 20's numbers are functions of the width |
Row four is the chapter's second subject and the one that makes reuse cost something. An agent that is width-independent is an agent whose derived numbers are wrong at three of the four widths it claims to support.
3. RTL 1 — The Agent Package and the UID Allocator
// ---------------------------------------------------------------------
// uvmagent_pkg -- the types an Ethernet agent needs, and the one
// parameter everything else in Module 20 turns out to depend on.
//
// BEAT_OCTETS is not a convenience. Chapter 20.4's residue dimension is
// (length - 4) mod BEAT_OCTETS, its offset dimension is
// start mod BEAT_OCTETS, and Chapter 20.1 Section 17's phase count is
// BEAT_OCTETS / gcd(period, BEAT_OCTETS). Three derived quantities in
// two chapters, all of them this one number.
// ---------------------------------------------------------------------
package uvmagent_pkg;
// The xMII family this agent supports, in octets per beat.
// 1 Gb/s GMII is 8 bits, 10 and 25 Gb/s are 64, 100 Gb/s CGMII is 512.
// Chapter 19.1 Section 4's table, converted.
typedef enum logic [1:0] {
W_GMII = 2'd0, // 1 octet per beat -- 1 Gb/s
W_XGMII = 2'd1, // 8 octets per beat -- 10 and 25 Gb/s
W_CGMII = 2'd2, // 64 octets per beat -- 100 Gb/s
W_FUTURE = 2'd3 // 128 octets per beat -- a projection, Section 10
} width_class_e;
function automatic int octets_of(width_class_e w);
case (w)
W_GMII: return 1;
W_XGMII: return 8;
W_CGMII: return 64;
default: return 128;
endcase
endfunction
// The identifier. 24 bits is 16.7 million frames, which at 100 Gb/s
// and minimum size is 112 milliseconds of wire time -- longer than
// any regression run in this track and short enough to be free.
typedef logic [23:0] frame_uid_t;
// What the agent knows about a frame at mint time. Every field here
// is a PLAN fact: what the sequence intended. Nothing observed.
typedef struct packed {
frame_uid_t uid;
logic [13:0] wire_len; // 64 to 9018, the wire length
logic [2:0] bucket; // Chapter 19.7 Section 2's RMON bucket
logic [1:0] tags; // 0, 1 or 2 VLAN tags
logic [2:0] err_class; // Chapter 7.3's five, plus none
logic [4:0] list_intent; // which of Chapter 20.1 Section 15's
// five lists this frame was FOR
} plan_item_t;
// What any component observed about a frame. Every field is stamped
// with the uid, which is the whole point of the package.
typedef struct packed {
frame_uid_t uid;
logic seen; // the monitor saw it
logic compared; // Chapter 20.3's scoreboard compared it
logic fired; // at least one property's antecedent hit
logic sampled; // Chapter 20.4's sampler took a cell
logic first_hit; // and the cell was new
logic [15:0] core_octets; // what the comparison actually covered
} observation_t;
// Chapter 20.3 Section 3's digest, reused verbatim as the join key.
// The agent does not define a new one -- defining a second digest is
// how the two halves stop agreeing.
typedef logic [31:0] digest_t;
endpackageClassification: a package whose entire content is one parameter and one identifier.
What it teaches: that BEAT_OCTETS is not a port width. It is the modulus of Chapter 20.4's residue dimension, the modulus of its offset dimension, and the argument of Chapter 20.1 §17's greatest common divisor. Three derived quantities in two chapters, and a team that treats the width as an interface detail will move it and leave all three behind. Section 6 is the audit.
And it teaches that the two structs are deliberately asymmetric. plan_item_t holds what the sequence intended; observation_t holds what somebody saw. The uid is the only field in both — and keeping them apart is what stops the join from becoming Chapter 20.4 §20's class 91, where the environment's own definition decides the measurement.
Deliberately simplified: frame_uid_t is a flat counter with no scope field, so two agents in one environment must be given disjoint ranges by hand — Section 14's second prohibition, unenforced here. W_FUTURE is a projection rather than a standard: 1024-bit datapaths exist in 400 Gb/s parts and the track's normative chapters stop at CGMII, so every number this chapter derives at 128 octets is arithmetic and not a citation. And list_intent is five bits of plan data that only the generator can fill, which makes the agent's join dependent on a component outside it.
Production implication: the 24-bit uid is the sizing decision to argue about. At 100 Gb/s, minimum size, back to back, a frame every 1.312 beats at 195.3125 MHz is 148.8 million frames per second — so 16.7 million uids is 112 ms. A soak longer than that wraps, and a wrapped uid joins two different frames' observations into one row, which is a silent corruption of exactly the number this chapter exists to produce. Thirty-two bits costs eight flops per item and buys 28.8 seconds.
// ---------------------------------------------------------------------
// uid_allocator -- mints one identifier per frame at the sequence
// layer, and records the digest it will be recoverable by.
//
// The monitor cannot be told the uid (a passive agent has no driver),
// so the allocator's second output is the digest that will be used to
// find it again. Section 4 is about what happens when two frames have
// the same one.
// ---------------------------------------------------------------------
module uid_allocator
import uvmagent_pkg::*;
#(
parameter int TABLE_DEPTH = 256 // frames in flight, Section 4
)(
input logic clk,
input logic rst_n,
// From the sequence layer.
input logic mint_req,
input plan_item_t mint_item, // uid field ignored on input
input digest_t mint_digest, // Chapter 20.3 Section 3's
output frame_uid_t minted_uid,
output logic minted_valid,
// Recovery, for the monitor.
input logic lookup_req,
input digest_t lookup_digest,
output frame_uid_t lookup_uid,
output logic lookup_hit,
output logic lookup_ambiguous,
// Health.
output logic [31:0] c_minted,
output logic [31:0] c_collisions,
output logic uid_wrapped
);
frame_uid_t next_uid;
digest_t d_tab [TABLE_DEPTH];
frame_uid_t u_tab [TABLE_DEPTH];
logic v_tab [TABLE_DEPTH];
logic [7:0] wr_ptr;
// Mint. The uid is a counter and nothing else -- it carries no
// meaning, which is what makes it safe to join on.
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
next_uid <= '0; wr_ptr <= '0; c_minted <= '0;
c_collisions <= '0; uid_wrapped <= 1'b0;
for (int i = 0; i < TABLE_DEPTH; i++) v_tab[i] <= 1'b0;
end else if (mint_req) begin
// A digest already in the table means two live frames share a
// join key. The mint still succeeds; the join is what breaks.
for (int i = 0; i < TABLE_DEPTH; i++)
if (v_tab[i] && d_tab[i] == mint_digest)
c_collisions <= c_collisions + 32'd1;
d_tab[wr_ptr] <= mint_digest;
u_tab[wr_ptr] <= next_uid;
v_tab[wr_ptr] <= 1'b1;
wr_ptr <= wr_ptr + 8'd1;
next_uid <= next_uid + 24'd1;
c_minted <= c_minted + 32'd1;
if (next_uid == '1) uid_wrapped <= 1'b1;
end
end
assign minted_uid = next_uid;
assign minted_valid = mint_req;
// Recovery. A linear search over the live table, which is a
// testbench structure and not a datapath one.
always_comb begin
lookup_uid = '0;
lookup_hit = 1'b0;
lookup_ambiguous = 1'b0;
for (int i = 0; i < TABLE_DEPTH; i++)
if (v_tab[i] && d_tab[i] == lookup_digest) begin
if (lookup_hit) lookup_ambiguous = 1'b1;
else lookup_uid = u_tab[i];
lookup_hit = 1'b1;
end
end
endmoduleClassification: a small associative table whose interesting output is the one that reports its own failure.
What it teaches: that minting and recovery are two different operations and only one of them can fail. Minting is a counter. Recovery is a search on a key the agent did not choose — Chapter 20.3 §3's digest is a function of the frame's invariant core, so two frames with the same core have the same key, and the allocator's job is to say so rather than to pretend otherwise.
And it teaches that lookup_ambiguous is more useful than lookup_hit. A miss is a frame the agent did not mint — a foreign frame, which in a passive agent is every frame — and that is ordinary. An ambiguous hit is two live frames the environment cannot tell apart, and every number Section 17 derives is wrong for both of them.
Deliberately simplified: the table is a fixed 256 entries with a wrapping write pointer and no eviction on completion, so a frame that retires leaves its digest live until 256 more are minted. The linear search is 256 comparators — fine in a testbench and absurd in silicon. And c_collisions counts collisions at mint time only, so two frames that collide with a third are counted twice.
Production implication: uid_wrapped is the bit that decides whether a soak run's joined numbers can be believed. It is not a warning; it is a verdict — once it sets, observation_t rows from before and after the wrap are indistinguishable, and Section 17's percentage becomes a mixture of two runs. The correct response is to fail the run, not to log it, which is Section 16's monitor.
4. The Identifier Has to Be Recoverable, Not Just Minted
Section 3 mints a uid and records a digest. This section is about how often two live frames have the same digest, and the answer is not a property of the digest function.
Chapter 20.3 §3's digest is computed over the invariant core — the part of the frame the design is not entitled to change — and Chapter 20.3 §2 measured that core on the traffic that matters:
| Frame | Core | Share |
|---|---|---|
| 64 octets, one payload octet | 15 octets | 23.4% |
| 64 octets, 46 payload octets | 60 octets | 93.8% |
| 1 518 octets | 1 514 octets | 99.7% |
Row one is a control frame — an acknowledgement, an ARP reply, a PAUSE — and its core is 14 octets of header and one octet of payload. In a stress stream the header does not vary: same destination, same source, same EtherType. So the join key's entire entropy is one octet, and the key space is 256 values.
The allocator's table holds 256 live frames.
| Value | |
|---|---|
| key space on control traffic | 256 |
| live frames in the table | 256 |
| expected distinct keys | 162.0 |
| frames sharing a key with another | 94 — 36.7% |
| probability a given lookup is ambiguous | 63.1% |
Two-thirds of lookups are ambiguous, and nothing is wrong with the digest. The same 15-octet core that makes Chapter 20.3's pass a quarter of a pass makes this chapter's join key almost useless — one cause, two consequences, in two different components, which is the kind of coupling only a shared identifier could have revealed and is the reason it is worth having one.
Now vary the payload.
| Control traffic | 46 varied payload octets | |
|---|---|---|
| key space | 256 | the digest's 2^32 |
| P(any collision in a full table) | certain | 0.000 76% |
| expected collisions per 10 000 000 frames | millions | 0.59 |
The ratio between those two columns is about eighty thousand and the frames are the same size. Which gives the rule:
The join key's collision rate is a property of the stimulus, not of the hash. A generator that varies one octet has a one-octet key however wide the digest is.
And the response is not a wider digest. Widening a 32-bit digest to 64 does nothing at all on control traffic — the input has eight bits of entropy — and costs two comparators per table entry.
| Response | Effect on control traffic |
|---|---|
| widen the digest to 64 bits | none |
| deepen the table | worse — more live frames, same key space |
| vary a core octet in the generator | the fix, and it is a stimulus change |
report lookup_ambiguous and exclude those frames | the honest fallback |
Row three is the fix and it is worth being precise about what it costs. Varying one header octet — the low octet of the source address, say — changes which frames Chapter 7.4's learning table holds and is therefore not free. Varying one payload octet is free and is what the agent should ask for.
Row four is what the agent must do regardless, because a passive agent on a link somebody else drives has no say in the stimulus at all.
5. RTL 2 — The Beat-Width Adapter
// ---------------------------------------------------------------------
// beat_width_adapter -- the one place in the environment that knows
// how many octets a beat holds.
//
// It exists because THREE derived quantities in Module 20 are
// functions of that number and all three are written as constants
// somewhere else:
// Chapter 20.4's residue dimension = (wire_len - 4) mod W
// Chapter 20.4's offset dimension = start_octet mod W
// Chapter 20.1 Section 17's phases = W / gcd(period, W)
// A width change that does not move all three leaves a coverage model
// measuring a modulus the design no longer has.
// ---------------------------------------------------------------------
module beat_width_adapter
import uvmagent_pkg::*;
#(
parameter width_class_e WCLASS = W_CGMII
)(
input logic clk,
input logic rst_n,
// A frame, described in WIRE octets -- the width-free description.
input logic frame_valid,
input logic [13:0] wire_len, // 64 to 9018
input logic [31:0] start_octet, // absolute octet index on the wire
// The same frame, described in BEATS at this width.
output logic [15:0] beats_occupied,
output logic [7:0] start_offset, // 0 .. W-1
output logic [7:0] residue, // (wire_len - 4) mod W
output logic spans_two_beats, // Chapter 19.4 Section 7's case
// The derived constants a coverage model needs and usually hardcodes.
output logic [7:0] offsets_declared, // W
output logic [7:0] offsets_reachable, // max(1, W/4)
output logic [7:0] phases_this_size, // W / gcd(period, W)
output logic size_is_degenerate // phases_this_size == 1
);
localparam int W = octets_of(WCLASS);
function automatic int gcd_of(int a, int b);
int x = a, y = b, t;
while (y != 0) begin t = y; y = x % y; x = t; end
return x;
endfunction
// Chapter 19.3 Section 6's deficit makes the gap 12 - (L mod 4), so
// the wire period is ALWAYS a multiple of four. That fact is what
// makes offsets_reachable W/4 and not W.
logic [15:0] period;
always_comb period = 16'(wire_len) + 16'd20 - 16'(wire_len % 14'd4);
always_comb begin
beats_occupied = 16'((wire_len + W - 1) / W);
start_offset = 8'(start_octet % 32'(W));
residue = 8'((wire_len - 14'd4) % 14'(W));
// Chapter 19.1's run F: at 16 octets a beat is SMALLER than the
// 20-octet gap, so two frames cannot share one. The condition is
// the beat against the inter-data spacing, not against the period.
spans_two_beats = (16'(W) > (period - 16'(wire_len)));
offsets_declared = 8'(W);
offsets_reachable = (W < 4) ? 8'd1 : 8'(W / 4);
phases_this_size = 8'(W / gcd_of(int'(period), W));
size_is_degenerate = (phases_this_size == 8'd1);
end
endmoduleClassification: a pure function block whose outputs are the constants three other chapters wrote down by hand.
What it teaches: that every one of Module 20's moduli is this parameter, and that the fact is invisible until somebody writes them in one place. Chapter 20.4's residue is mod W, its offset is mod W, Chapter 20.1 §17's phase count is W / gcd(period, W) — three expressions, one number, and in the published environments they are three separate 64s.
And it teaches where offsets_reachable comes from, which is the least obvious line in the block. Chapter 19.3 §6's deficit chooses the interframe gap as 12 − (L mod 4), so the wire period L + 20 − (L mod 4) is always divisible by four, whatever L is. Every frame boundary therefore lands on an octet index that is a multiple of four — so of the W declared offsets, only W/4 can ever occur. That is Chapter 20.1 §9's "sixteen of sixty-four" derived as a general law rather than measured at one width.
Deliberately simplified: gcd_of is a run-time Euclid loop rather than an elaboration-time constant, which is correct and is not synthesisable as written — a real adapter computes the phase count in a localparam table indexed by size class. residue uses a 14-bit modulus operation that will not fold cleanly at W = 128. And spans_two_beats is a property of the size, not of the individual frame: Chapter 19.4 §7's dual-frame beat depends on where the stream happens to be, and this output says only whether it is possible.
Production implication: size_is_degenerate is the output worth wiring to a test's front door. A fixed-size stream visits W / gcd(period, W) beat offsets — and at 1 518 octets the period is 1 536, which is divisible by 8, by 64 and by 128, so the most common maximum-size stress test in Ethernet verification visits exactly one offset at every width in the family. The bit is one comparator and it turns a silent coverage hole into a message at time zero.
6. Every Modulus in Module 20 Is the Beat Width
Module 20 contains eleven constants that look like independent design choices and are the same number.
| Where | Written as | Actually |
|---|---|---|
| Chapter 20.4 §2's residue dimension | 64 values | W |
| Chapter 20.4 §2's offset dimension | 64 values | W |
| Chapter 20.4 §2's dual-frame dimension | 2 values | 2 if W > 20, else 1 |
| Chapter 20.4 §4's bucket 0 residues | 1 of 64 | 1 of W |
| Chapter 20.4 §4's bucket 1 residues | 63 of 64 | min(63, W) |
| Chapter 20.4 §4's loopback offsets | 16 | max(1, W/4) |
| Chapter 20.1 §9's reachable phases | 16 of 64 | max(1, W/4) of W |
Chapter 20.1 §15's residues_count target | 64 | W |
Chapter 20.1 §15's phases_count target | 64 | W |
Chapter 20.1 §15's c_spill_sizes target | 23 | the count of sizes ≡ 3 (mod W) |
| Chapter 20.1 §17's phase count | 64 / gcd(period, 64) | W / gcd(period, W) |
Eleven constants, one parameter, and every one of them is written as a literal in a different file. That is what Section 5's adapter is for, and it is the whole technical content of the phrase "works across the xMII family."
Three of the eleven deserve their own derivation because they are not proportional to W.
Row three — the dual-frame dimension. Chapter 19.4 §7's dual-frame beat needs a beat that holds data from two frames, which needs the beat to be wider than the space between them. That space is the preamble and the gap: 20 − (L mod 4) octets, so 17 to 20.
| Width | Beat, octets | Gap plus preamble | Dual-frame beat possible? |
|---|---|---|---|
| 1 Gb/s GMII | 1 | 17 to 20 | no |
| 10 and 25 Gb/s | 8 | 17 to 20 | no |
| 100 Gb/s CGMII | 64 | 17 to 20 | yes |
| a 1 024-bit projection | 128 | 17 to 20 | yes |
Chapter 19.1 §20's run F already said this and said it about a different question. At 128 bits the beat is 16 octets, below the 20-octet gap, so dual-frame beats cannot occur — and the same sentence, read as arithmetic rather than as a control experiment, removes a whole dimension from the coverage model at two of the four widths.
Row six — the reachable offsets. Chapter 19.3 §6's deficit sets the gap to 12 − (L mod 4), so the wire period L + 20 − (L mod 4) is always a multiple of four. Frame boundaries therefore land only on octet indices that are multiples of four, so of W declared offsets only W/4 occur — at every width, for every size, permanently.
| Width | Declared offsets | Reachable | Share |
|---|---|---|---|
| 1 | 1 | 1 | 100% |
| 8 | 8 | 2 | 25% |
| 64 | 64 | 16 | 25% |
| 128 | 128 | 32 | 25% |
The 25% is the same at every width above one, which is the first genuinely width-invariant number in this chapter and Section 18 collects the rest.
Row ten — the spill sizes — is the one that does not scale at all. Chapter 19.3 §20's spill case is a frame whose check value crosses a beat boundary: pre-FCS length ≡ W − 1 (mod W), which on the wire is L ≡ 3 (mod W). Counting those between 64 and 1 518:
| Width | Spill sizes in range | Chapter 20.1 §15's target is "more than 23" |
|---|---|---|
| 1 | 0 — the case does not exist | UNREACHABLE |
| 8 | 182 | met by 12.5% of a uniform sweep |
| 64 | 23 | UNREACHABLE by one — the target was written as a strict inequality |
| 128 | 11 | UNREACHABLE — only 11 exist |
Row one first, because it is not a scaling failure but a category one. At one octet per beat the check value occupies four whole beats and never crosses a boundary — Chapter 19.3 §4's spill case has no instances at GMII, so a goal expressed as a count of spill sizes is not small there; it is meaningless.
Row four is the sharpest consequence of reuse in the chapter. An agent that carries Chapter 20.1 §15's telemetry unchanged to a 1 024-bit datapath sets a goal of more than 23 distinct spill sizes in a space that contains 11. The list never completes, list_coverage_pct caps at 80%, and the run reads as a stimulus problem forever.
Row three is the same hazard at the width the constant was written for. c_spill_sizes > 23 is a strict inequality over a set of exactly 23 members: the goal is one short of reachable at 100 Gb/s too, and the only reason nobody noticed is that list_coverage_pct at 80% looks like an unfinished run rather than an impossible one. Section 16's monitor exists because this class of mistake is invisible to every validator in the environment.
A reused threshold is only reused if the quantity it bounds is reused. Two of Chapter 20.1 §15's five are counts of a set whose size is a function of
W.
7. RTL 3 — The Agent Monitor
// ---------------------------------------------------------------------
// agent_monitor -- watches one interface and produces observation_t
// rows keyed on the uid.
//
// It cannot be TOLD the uid. In a passive configuration there is no
// driver and no item, so the only thing it has is octets -- and the
// only key derivable from octets is Chapter 20.3 Section 3's digest.
// Section 4 is why that key is weaker than it looks.
// ---------------------------------------------------------------------
module agent_monitor
import uvmagent_pkg::*;
#(
parameter width_class_e WCLASS = W_CGMII,
parameter bit IS_ACTIVE = 1'b0
)(
input logic clk,
input logic rst_n,
// The interface, at this width.
input logic beat_valid,
input logic frame_start,
input logic frame_end,
input logic [13:0] wire_len,
input logic [31:0] start_octet,
input digest_t core_digest, // Chapter 20.3 Section 3's
input logic [15:0] core_octets,
// What the rest of the environment saw about this frame.
input logic sb_compared,
input logic prop_fired,
input logic cov_sampled,
input logic cov_first_hit,
// Recovery, against Section 3's allocator.
output logic lookup_req,
output digest_t lookup_digest,
input frame_uid_t lookup_uid,
input logic lookup_hit,
input logic lookup_ambiguous,
// The row.
output observation_t obs,
output logic obs_valid,
// Health.
output logic [31:0] c_observed,
output logic [31:0] c_unattributed,
output logic [31:0] c_ambiguous
);
assign lookup_req = frame_end && beat_valid;
assign lookup_digest = core_digest;
always_comb begin
obs = '0;
obs.uid = lookup_uid;
obs.seen = 1'b1;
obs.compared = sb_compared;
obs.fired = prop_fired;
obs.sampled = cov_sampled;
obs.first_hit = cov_first_hit;
obs.core_octets = core_octets;
// An ambiguous key is NOT a row. Emitting it would attribute one
// frame's observations to another frame's uid, which corrupts
// Section 17's percentage in a direction nobody can see.
obs_valid = lookup_req && lookup_hit && !lookup_ambiguous;
end
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
c_observed <= '0; c_unattributed <= '0; c_ambiguous <= '0;
end else if (lookup_req) begin
c_observed <= c_observed + 32'd1;
if (!lookup_hit) c_unattributed <= c_unattributed + 32'd1;
if (lookup_ambiguous) c_ambiguous <= c_ambiguous + 32'd1;
end
end
endmoduleClassification: an observer whose correctness rests on refusing to guess.
What it teaches: that obs_valid is a conjunction of three terms and the third one is the interesting one. A hit is required, obviously. An ambiguous hit must be rejected, and the reason is asymmetric: a dropped row makes Section 17's denominator smaller and the percentage still means something; a wrongly attributed row makes the percentage a mixture of two frames and means nothing. Dropping is recoverable; guessing is not.
And it teaches that IS_ACTIVE changes nothing in this block. A passive monitor on a link the environment does not drive produces c_unattributed for every frame and no rows at all — which is correct and is worth being explicit about, because an agent that silently produces no observations in passive mode looks identical to one that is not bound.
Deliberately simplified: the monitor takes sb_compared, prop_fired and cov_sampled as inputs, which assumes those three components can present their verdict at frame_end. Chapter 20.3 §11's reorder window is 631 cycles deep, so a real scoreboard's verdict arrives long after the frame does, and the join has to be deferred — Section 13's table is where that deferral lives. The block as written is the synchronous idealisation.
Production implication: c_unattributed against c_observed is the ratio that tells a team whether its agent is bound to the interface it thinks it is. In an active agent it should be zero; anything else means frames are arriving that this environment did not generate — a leftover background driver, a second agent sharing the uid space, or Chapter 20.2 §10's silent bind failure appearing from the other side. In a passive agent it should be one hundred per cent, and a passive agent reporting less than that has found a frame it minted, which is impossible and therefore a wiring error.
8. The Cross Is Quadratic in the Beat Width
Chapter 20.4's model has six dimensions and two of them are the beat width. So the declared space is W² times everything else, and moving one interface moves the coverage model by a square.
Rebuild Chapter 20.4 §4's arithmetic with W in it.
declared = 7 buckets × W residues × W offsets × 3 tag counts
× 5 error classes × D where D = 2 if W > 20 else 1
legal = Σ over buckets [ min(sizes_in_bucket, W) × errors_in_bucket ]
× W offsets × 3 × D
reachable (loopback) = the same sum × max(1, W/4) offsets × 3 × DAt W = 64 that is 860 160, 467 328 and 116 832 — Chapter 20.4 §4's three numbers, reproduced. Which is the check that the generalisation is the same model and not a new one.
| Width | Declared | Legal | Legal share | Loopback reachable | Of legal |
|---|---|---|---|---|---|
| 1 — GMII | 105 | 69 | 65.7% | 69 | 100.0% |
| 8 — 10 and 25 Gb/s | 6 720 | 3 744 | 55.7% | 936 | 25.0% |
| 64 — CGMII | 860 160 | 467 328 | 54.3% | 116 832 | 25.0% |
| 128 — a projection | 3 440 640 | 1 721 088 | 50.0% | 430 272 | 25.0% |
Four things in that table are worth naming separately.
First, the declared column moves by 128× between 10 Gb/s and 100 Gb/s — 64× from W² and 2× from the dual-frame dimension appearing. A coverage model ported from a 10 Gb/s environment to a 100 Gb/s one without re-deriving is measuring 6 720 cells of a space that has 860 160, and it will close.
Second, the legal share falls as the width grows — 65.7%, 55.7%, 54.3%, 50.0% — and the mechanism is bucket 0 and bucket 1. Bucket 0 is the single size 64, so it has one residue whatever W is; bucket 1 spans 63 sizes, so it saturates at 63. As W grows past 63 those two buckets stop contributing and the illegal fraction grows, which is why the share is still falling at 128 and would keep falling.
Third, the loopback reaches exactly 25.0% of legal at every width above one — the W/4 of Section 6, unchanged. That is the number a closure target should be set against and it is width-invariant, which is what Chapter 20.4 §19 wanted from pct_of_legal and did not have a reason for.
Fourth, GMII is a different regime and not a smaller one. At one octet per beat there is no residue dimension, no offset dimension and no dual-frame dimension — a beat is an octet, a frame starts where it starts, and two frames cannot share a beat. The cross collapses from six dimensions to three and from 860 160 cells to 105, and a verification plan that describes both as "the coverage model" is describing two different objects.
| CGMII | GMII | |
|---|---|---|
| live dimensions | 6 | 3 |
| declared cells | 860 160 | 105 |
| ratio | 8 192× | — |
| what closure means | a week of soak | a few thousand frames |
And that ratio is the honest answer to "does this agent work at 1 Gb/s." The sequence layer does. The coverage model does not exist at 1 Gb/s in the form Chapter 20.4 wrote it, because three of its six dimensions are degenerate — and an agent that reports pct_of_declared against 105 cells alongside one against 860 160 is reporting two incomparable numbers under one name.
9. RTL 4 — The Sequence Layer
// ---------------------------------------------------------------------
// agent_sequence_layer -- the half of the agent that does NOT change
// with the beat width, which is the whole reuse claim, stated as a
// module boundary rather than as a promise.
//
// Everything here is expressed in WIRE octets: a length, a tag count,
// an error class, a gap. Nothing in this block names a beat. That is
// the test of whether the sequence layer is really width-independent,
// and it is a test the block either passes structurally or fails.
// ---------------------------------------------------------------------
module agent_sequence_layer
import uvmagent_pkg::*;
#(
parameter int SEED = 1
)(
input logic clk,
input logic rst_n,
input logic pull, // the driver is ready for an item
// The plan, in width-free terms.
input logic [13:0] len_min,
input logic [13:0] len_max,
input logic [7:0] w_runt_pct,
input logic [7:0] w_spill_pct,
input logic [1:0] tags_max,
input logic allow_illegal,
// The one width-dependent input, and it is a VALUE not a behaviour.
input logic [7:0] spill_modulus, // = W, from Section 5's adapter
output plan_item_t item,
output logic item_valid,
output logic [31:0] c_emitted,
output logic spill_target_impossible
);
logic [31:0] lfsr;
logic [13:0] draw_len;
logic [7:0] roll;
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) lfsr <= 32'(SEED);
else if (pull) lfsr <= {lfsr[30:0], lfsr[31] ^ lfsr[21] ^ lfsr[1] ^ lfsr[0]};
end
assign roll = lfsr[7:0];
// The spill target is the only place the width reaches the sequence
// layer, and it reaches it as a MODULUS rather than as a beat count.
// Section 6: the set is the sizes congruent to 3 modulo the width.
always_comb begin
draw_len = len_min + 14'(lfsr[29:16] % 14'((len_max - len_min) + 1));
if (roll < w_spill_pct && spill_modulus != 8'd0)
draw_len = draw_len - 14'(draw_len % 14'(spill_modulus)) + 14'd3;
if (allow_illegal && roll >= (8'd100 - w_runt_pct))
draw_len = 14'd5 + 14'(lfsr[13:0] % 14'd43); // the useful runts
end
always_comb begin
item = '0;
item.wire_len = draw_len;
item.tags = 2'(lfsr[19:18] % 2'(tags_max + 1));
item.err_class = 3'd0;
item.list_intent = {1'b0,
(roll < w_runt_pct),
1'b0,
(roll < w_spill_pct),
1'b1};
item_valid = pull;
end
always_ff @(posedge clk or negedge rst_n)
if (!rst_n) c_emitted <= '0;
else if (pull) c_emitted <= c_emitted + 32'd1;
// Section 6's row four, made loud at elaboration rather than found
// at the end of a week's soak.
localparam int LEN_SPAN = 1455;
assign spill_target_impossible =
(spill_modulus == 8'd0) ||
((32'(LEN_SPAN) / 32'(spill_modulus)) <= 32'd23);
endmoduleClassification: a stimulus source whose design goal is that one input is a number and not a structure.
What it teaches: that "width-independent" is a structural property you can check by grep. No signal in this block is named beat, no arithmetic in it divides by a beat count, and the one width-dependent quantity enters as spill_modulus — a value. A sequence layer that took a beat width and computed beats from it would be width-parameterised, which is a different and much weaker thing: it would still have to be reviewed at every width.
And it teaches why spill_target_impossible is in the sequence layer rather than in the telemetry. The impossibility is a fact about the plan — about a range and a modulus — and it is known at elaboration. Reporting it in the telemetry means finding it after a run; reporting it here means the run does not start. Section 6's rows one, three and four are all caught by this one comparison.
Deliberately simplified: the length draw is a modulo of an LFSR, so it is biased whenever the span does not divide 2^14 — acceptable for a chapter and not for a regression. The spill adjustment can push draw_len below len_min, which is unguarded. list_intent sets bit 0 unconditionally, meaning every frame claims to serve the residue list, which is true and useless. And err_class is hardwired to zero — Chapter 20.5's injector is a separate component and the plan item cannot currently record that a frame was meant to be broken.
Production implication: the last item is the one to fix first. Chapter 20.5 §8 showed that the injector's intent and the design's classification legitimately differ, and Section 13's join is the only place in the environment where both are present at once. A plan_item_t that cannot record "this frame was meant to be a runt" cannot answer "was the frame the design classified as undersize the one we aimed at" — which is Chapter 20.5 §20's rejected property turned into a measurement instead of an assertion.
10. What "Works Across the xMII Family" Costs, and What Survives
The phrase in the module's blurb is "works across the xMII family without rewriting the sequence layer." Section 9 shows the sequence layer really is unchanged. This section prices everything else.
Take the agent's eight blocks and ask of each one whether a width change touches it.
| Block | Width-dependent? | What moves |
|---|---|---|
| Section 3's package | one enum value | octets_of, and nothing else |
| Section 3's uid allocator | no | a uid is a counter |
| Section 5's beat-width adapter | entirely | it is the width |
| Section 7's monitor | partly | the offset and residue it reports |
| Section 9's sequence layer | no | one input value — Section 9 |
| Section 11's driver | entirely | beat assembly is the width |
| Section 13's observation join | no | it joins on a uid |
| Section 15's telemetry | its thresholds | Section 12 |
Three of eight are untouched, two are entirely the width, and three are partial. Which is the honest version of the reuse claim: the agent is about 60% reusable across the family and the 40% is concentrated in two blocks — and that is a good outcome, because the alternative is 100% of the environment being 10% touched, which is the shape that cannot be reviewed.
But the blocks are the cheap part. The expensive part is the derived numbers, and they are not in the agent at all.
| Derived number | Lives in | Moves with W? |
|---|---|---|
| the declared cross | Chapter 20.4 §3 | yes — as W² |
| the legal cross | Chapter 20.4 §4 | yes, and not proportionally |
| the reachable offsets | Chapter 20.1 §9 | yes — W/4 |
| the spill-size target | Chapter 20.1 §15 | yes, and inverts |
| the phase-degeneracy rule | Chapter 20.1 §17 | yes — gcd with W |
| the runt threshold, 43 | Chapter 19.7 §7 | no |
| the reorder target, 4 | Chapter 19.6 §21 | no |
| the invariant core share | Chapter 20.3 §2 | no |
Five move and three do not, and they are distributed across four chapters and none of them is inside the agent. So the real cost of supporting a new width is not editing the agent — it is finding the five constants, and the reason Section 5's adapter exists is to make that a compile-time dependency instead of a search.
And three things survive a width change untouched, which is worth stating because they are the things a plan can actually rely on.
| Survives | Value | Why |
|---|---|---|
| the loopback's share of legal | 25.0% | W/4 of W — Section 6 |
| the 1 516 to 1 519 degeneracy | 1 phase, every width | period 1 536 divides by 8, 64 and 128 |
| the runt useful range | 43 lengths | Chapter 19.7 §7's 48 is octets, not beats |
Row two is the one to put in a checklist. A fixed-size stress test at 1 516, 1 517, 1 518 or 1 519 octets visits exactly one beat offset at every width in the family — because all four have the same wire period, 1 536, and 1 536 is divisible by 8, by 64 and by 128. The same test at 1 520 visits every reachable offset at every width, because 1 540 is 4 mod 8. Four octets of frame size, and the difference is between one offset and all of them, at 10 Gb/s, at 100 Gb/s and at a width that does not exist yet.
11. RTL 5 — The Driver
// ---------------------------------------------------------------------
// agent_driver -- turns a width-free plan item into beats at this
// width. This is the half of the agent that IS the width, and keeping
// it separable from Section 9 is the entire structural claim.
//
// It also does the one thing only a driver can do: it stamps the
// digest at the moment the frame is committed to the wire, so the
// monitor's later lookup is against the bytes that were actually sent
// rather than against the bytes that were planned.
// ---------------------------------------------------------------------
module agent_driver
import uvmagent_pkg::*;
#(
parameter width_class_e WCLASS = W_CGMII
)(
input logic clk,
input logic rst_n,
input plan_item_t item,
input logic item_valid,
output logic pull,
// From Section 5, so the driver does not recompute the width.
input logic [15:0] beats_occupied,
input logic [7:0] start_offset,
// The wire.
output logic beat_valid,
output logic frame_start,
output logic frame_end,
output logic [7:0] valid_octets, // 1 .. W on the last beat
output logic [31:0] octets_sent,
// The join key, stamped at commit.
output digest_t commit_digest,
output logic commit_valid,
output logic [31:0] c_driven,
output logic gap_underrun
);
localparam int W = octets_of(WCLASS);
logic [15:0] beats_left;
logic [7:0] gap_left;
logic in_frame;
// Chapter 19.3 Section 6's deficit, expressed in OCTETS and then
// converted -- not computed in beats, because at W = 64 the whole
// gap is smaller than one beat and beat arithmetic loses it.
logic [7:0] gap_octets;
always_comb gap_octets = 8'd12 - 8'(item.wire_len % 14'd4);
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
beats_left <= '0; gap_left <= '0; in_frame <= 1'b0;
octets_sent <= '0; c_driven <= '0; gap_underrun <= 1'b0;
end else if (in_frame) begin
beats_left <= beats_left - 16'd1;
octets_sent <= octets_sent + 32'(W);
if (beats_left == 16'd1) begin
in_frame <= 1'b0;
gap_left <= gap_octets;
c_driven <= c_driven + 32'd1;
end
end else if (gap_left != 8'd0) begin
gap_left <= (gap_left > 8'(W)) ? (gap_left - 8'(W)) : 8'd0;
octets_sent <= octets_sent + 32'(W);
// Chapter 5.9's floor is nine octets. At W = 64 a single idle
// beat overshoots it by 52, which is legal and is why the
// deficit exists at all.
if (gap_left < 8'd9 && 8'(W) > gap_left) gap_underrun <= 1'b0;
end else if (item_valid) begin
in_frame <= 1'b1;
beats_left <= beats_occupied;
end
end
assign pull = !in_frame && (gap_left == 8'd0);
assign beat_valid = in_frame;
assign frame_start = in_frame && (beats_left == beats_occupied);
assign frame_end = in_frame && (beats_left == 16'd1);
assign valid_octets = frame_end
? 8'(((item.wire_len - 14'd1) % 14'(W)) + 14'd1)
: 8'(W);
assign commit_digest = digest_t'(item.uid) ^ digest_t'(item.wire_len);
assign commit_valid = frame_end;
endmoduleClassification: a width-bound shaper whose one width-free decision is the gap.
What it teaches: that the interframe gap must be computed in octets and converted, never computed in beats. Chapter 19.3 §6's deficit is 12 − (L mod 4) octets — nine to twelve — and at W = 64 that is between 0.14 and 0.19 of a beat. A driver that holds a gap counter in beats rounds it to one beat, emits 64 octets of idle where 9 were required, and produces a stream whose period is L + 8 + 64 instead of L + 20 − (L mod 4) — which changes every phase number in Section 6's table and is invisible on a waveform.
And it teaches that pull is the reuse boundary made into a signal. The sequence layer produces an item whenever the driver asks; the driver asks when the wire is ready, which is a width-dependent moment. No back-pressure crosses the boundary in the other direction, so the sequence layer cannot observe the width even by timing — which is a stronger form of independence than "does not read the parameter."
Deliberately simplified: commit_digest is a two-term XOR standing in for Chapter 20.3 §3's real digest over the invariant core, and being a function of the uid it cannot collide when the real one would — so this driver hides exactly the problem Section 4 is about. gap_underrun is assigned only to zero and is therefore dead, which is deliberate: the condition it wants is gap_left reaching zero before nine octets have passed, and expressing it needs an octet counter the block does not have. And valid_octets is wrong for W = 1, where the modulo yields 1 always and happens to be right for the wrong reason.
Production implication: the dead gap_underrun is the shape of most real driver bugs and it is worth looking at rather than fixing silently. A gap violation is Chapter 20.5 §9's injection when it is deliberate and a driver defect when it is not, and the two are indistinguishable at the interface. The only way to tell them apart is a flag from the component that intended it — which is list_intent in Section 3's plan_item_t, and is the second thing Section 13's join is for.
12. Two Thresholds of Five Move, and Not by Scaling
Chapter 20.1 §15 reports five bits. Under a width change, two of the five move, and neither moves proportionally.
| Bit | Target at W = 64 | General form | Moves? |
|---|---|---|---|
l_residues | residues_count == 64 | == W | yes — linear |
l_spill | c_spill_sizes > 23 | one less than the count of sizes ≡ 3 mod W | yes — inverse |
l_runts | c_runts > 43 | unchanged | no |
l_phases | phases_count == 64 | == W, reachable W/4 | yes — linear |
l_reorder | peak_held >= 4 | unchanged | no |
Rows one and four move linearly and are the easy case: a target of "all of them" stays "all of them" and only the literal changes. Row two is the hard case and it moves in the opposite direction to the width, which is the single most counter-intuitive consequence of the beat width in Module 20.
| Width | Spill sizes that exist | Target > 23 | Reachable? |
|---|---|---|---|
| 1 | 0 | > 23 | no — the case does not exist |
| 8 | 182 | > 23 | yes, easily |
| 64 | 23 | > 23 | no — by exactly one |
| 128 | 11 | > 23 | no |
The same constant is easy at 10 Gb/s, impossible by one at 100 Gb/s and impossible by twelve at 128 octets — and the widths where it is impossible are the two where the spill case actually matters, because a wider beat is what makes a four-octet check value straddle a boundary in the first place.
Row four of the first table — the phase bit — has a second failure the linear form hides.
Chapter 20.1 §15 also reports topology_limited, which compares phases_count against 16. Generalised, that is W/4. But the distribution of what a fixed-size stream reaches changes shape with the width:
| Width | Reachable phases | Sizes reaching all of them | Sizes reaching exactly one |
|---|---|---|---|
| 8 | 2 | 728 — 50.0% | 727 — 50.0% |
| 64 | 16 | 728 — 50.0% | 91 — 6.3% |
| 128 | 32 | 728 — 50.0% | 47 — 3.2% |
The "reaches all" column is 50.0% at every width and the "reaches one" column is not, because at W = 8 there are only two phases, so failing to reach all of them is reaching one. Half of all frame sizes are maximally degenerate at 10 Gb/s and 6.3% are at 100 Gb/s — so a fixed-size test migrated upward looks like it improved when the space it is failing to cover merely got finer.
At W = 8 | At W = 64 | |
|---|---|---|
| a size reaching one phase | 50% of the offsets | 6.25% of the offsets |
| how it reports | "half covered" | "a sixteenth covered" |
| what changed | nothing about the test | nothing about the test |
And the mean across all sizes is 75.0% of reachable phases at W = 8 against 66.8% at W = 64 — so the average fixed-size test genuinely does cover a larger share of a smaller space at the lower rate, and neither number is a statement about the design.
13. RTL 6 — The Observation Join
// ---------------------------------------------------------------------
// observation_join -- the block this chapter exists for.
//
// Chapter 20.4 Section 26: four components produce four numbers and
// share no identifier. This is the table where the identifier lives.
// Every row is one frame; every column is one component's verdict
// about that frame; and the interesting outputs are the ones no single
// component could have produced.
//
// It is deliberately NOT width-parameterised. A uid is a uid.
// ---------------------------------------------------------------------
module observation_join
import uvmagent_pkg::*;
#(
parameter int ROWS = 1024 // deferred verdicts, Section 7
)(
input logic clk,
input logic rst_n,
// A frame was planned.
input logic plan_valid,
input plan_item_t plan,
// Somebody observed something about a frame.
input logic obs_valid,
input observation_t obs,
// A row retires when its scoreboard verdict finally lands --
// Chapter 20.3 Section 11's window is 631 cycles at best.
input logic retire_valid,
input frame_uid_t retire_uid,
// The four joined counts.
output logic [31:0] c_rows_closed,
output logic [31:0] c_first_hit, // closed a new coverage cell
output logic [31:0] c_first_hit_checked,// ... AND was compared
output logic [31:0] c_first_hit_fired, // ... AND fired a property
output logic [31:0] c_first_hit_bare, // ... and neither
output logic [31:0] c_core_octets,
// Health of the join itself.
output logic [31:0] c_orphan_obs, // an observation, no plan
output logic [31:0] c_orphan_plan, // a plan, no observation
output logic table_overflow
);
frame_uid_t r_uid [ROWS];
logic r_live [ROWS];
logic r_plan [ROWS];
observation_t r_obs [ROWS];
logic r_obsd [ROWS];
function automatic int slot_of(frame_uid_t u);
return int'(u) % ROWS; // direct-mapped, Section 14
endfunction
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
c_rows_closed <= '0; c_first_hit <= '0; c_first_hit_checked <= '0;
c_first_hit_fired <= '0; c_first_hit_bare <= '0; c_core_octets <= '0;
c_orphan_obs <= '0; c_orphan_plan <= '0; table_overflow <= 1'b0;
for (int i = 0; i < ROWS; i++) begin
r_live[i] <= 1'b0; r_plan[i] <= 1'b0; r_obsd[i] <= 1'b0;
end
end else begin
if (plan_valid) begin
// A live row being overwritten is a lost join, not a lost
// frame. It is reported because the alternative is a
// percentage computed over a silently smaller denominator.
if (r_live[slot_of(plan.uid)]) table_overflow <= 1'b1;
r_uid [slot_of(plan.uid)] <= plan.uid;
r_live[slot_of(plan.uid)] <= 1'b1;
r_plan[slot_of(plan.uid)] <= 1'b1;
r_obsd[slot_of(plan.uid)] <= 1'b0;
end
if (obs_valid) begin
if (r_live[slot_of(obs.uid)] && r_uid[slot_of(obs.uid)] == obs.uid) begin
r_obs [slot_of(obs.uid)] <= obs;
r_obsd[slot_of(obs.uid)] <= 1'b1;
end else begin
c_orphan_obs <= c_orphan_obs + 32'd1;
end
end
if (retire_valid && r_live[slot_of(retire_uid)] &&
r_uid[slot_of(retire_uid)] == retire_uid) begin
r_live[slot_of(retire_uid)] <= 1'b0;
c_rows_closed <= c_rows_closed + 32'd1;
if (!r_obsd[slot_of(retire_uid)]) begin
c_orphan_plan <= c_orphan_plan + 32'd1;
end else begin
c_core_octets <= c_core_octets +
32'(r_obs[slot_of(retire_uid)].core_octets);
if (r_obs[slot_of(retire_uid)].first_hit) begin
c_first_hit <= c_first_hit + 32'd1;
if (r_obs[slot_of(retire_uid)].compared)
c_first_hit_checked <= c_first_hit_checked + 32'd1;
if (r_obs[slot_of(retire_uid)].fired)
c_first_hit_fired <= c_first_hit_fired + 32'd1;
if (!r_obs[slot_of(retire_uid)].compared &&
!r_obs[slot_of(retire_uid)].fired)
c_first_hit_bare <= c_first_hit_bare + 32'd1;
end
end
end
end
end
endmoduleClassification: a direct-mapped table whose value is entirely in four counters that no other block in Module 20 could have produced.
What it teaches: that c_first_hit_bare is the number this chapter was built to make. It counts frames that closed a new coverage cell and were neither compared by a scoreboard nor seen by any property's antecedent — cells whose only evidence is that a sampler fired. Chapter 20.4 §18 said closure is evidence of stimulus and not of correctness; this counter says how much of a given closure is in that state, and it is not derivable from any of the four components' own reports.
And it teaches why retirement is separate from observation. Chapter 20.3 §11's verdict is 631 cycles behind the frame at best and 100 000 at the window's setting — so a join that closed a row when the monitor saw the frame would record every scoreboard verdict as absent. The three-phase life of a row — planned, observed, retired — is the shape the latency forces, and getting it wrong makes c_first_hit_checked read zero for a working environment.
Deliberately simplified: the table is direct-mapped on uid mod ROWS, so a frame minted 1 024 uids after a live one evicts it — Section 14's first prohibition, reported by table_overflow and not prevented. slot_of is called eleven times and a synthesiser will share it; a reader will not. Rows are never timed out, so a frame that is legally dropped and never retires holds its slot forever. And c_orphan_plan conflates two different things — a frame the monitor never saw and a frame whose digest was ambiguous — which are a lost frame and a lost key.
Production implication: c_orphan_obs and c_orphan_plan together are the join's own error bar, and Section 17's percentage is meaningless without them. A run whose orphan counts are 30% of rows has a join over 70% of the traffic, and the 7.4% it reports is 7.4% of that 70%. The discipline is the same one Chapter 20.3 §14 applied to the scoreboard: report the share the number was computed over, because a percentage whose denominator moved is Chapter 20.4 §20's class 91 wearing a different hat.
14. What an Agent Must Never Do
Six prohibitions. Four are about the identifier and two are about the width, and all six produce a number that looks fine.
| Never | Because | |
|---|---|---|
| 1 | let two live frames share a join slot | Section 13's direct map silently evicts |
| 2 | share a uid space between two agents | the join attributes one link's frames to another's |
| 3 | let the monitor guess on an ambiguous digest | Section 7 — a wrong row is worse than no row |
| 4 | modify the frame to carry the identifier | Section 2 — it changes which paths the frame takes |
| 5 | hardcode a beat count anywhere outside Section 5 | Section 6's eleven constants |
| 6 | report a percentage without its orphan counts | Section 13 — the denominator moved |
Row four is the one that gets violated first and it is violated for a good reason. Putting a sequence number in the payload makes the join exact, free and collision-proof. It also makes every frame 4 octets longer, which moves the size bucket, moves the residue, moves whether the check value spills, and changes Chapter 7.4's filtering decisions if the field lands in a header. The stimulus is no longer the stimulus that was planned — and the agent has bought an exact join over a different experiment.
Row two is the one that gets violated quietly. Two agents, one on each side of a link, each minting uids from zero — the join table sees two frames with uid 7 and joins them. c_orphan_obs does not move, because both lookups hit; the rows are simply wrong, and the environment reports a number rather than a failure. The fix is a scope field in frame_uid_t and Section 3 does not have one, which is the package's most consequential simplification.
And the two prohibitions that look like tuning advice and are not:
| Why it is a prohibition | |
|---|---|
| row five | a hardcoded 64 is correct at one width and wrong at three |
| row six | an unreported orphan share turns a measurement into an estimate |
Both produce a number nobody can check from the outside, which is the property all six share and is why they belong in a section rather than in a comment.
15. RTL 7 — Agent Telemetry
// ---------------------------------------------------------------------
// agent_telemetry -- the numbers the join makes possible, and the two
// numbers about the join itself without which they mean nothing.
//
// Three groups, split by what a reader does with each:
// Joined: what Module 20's four components say TOGETHER.
// Quality: what share of the traffic the join actually covers.
// Width: which of Section 6's constants this build is using.
// ---------------------------------------------------------------------
module agent_telemetry
import uvmagent_pkg::*;
#(
parameter width_class_e WCLASS = W_CGMII
)(
input logic clk,
input logic rst_n,
input logic [31:0] c_rows_closed,
input logic [31:0] c_first_hit,
input logic [31:0] c_first_hit_checked,
input logic [31:0] c_first_hit_fired,
input logic [31:0] c_first_hit_bare,
input logic [31:0] c_core_octets,
input logic [31:0] c_wire_octets,
input logic [31:0] c_orphan_obs,
input logic [31:0] c_orphan_plan,
input logic [31:0] c_ambiguous,
input logic [31:0] c_observed,
input logic uid_wrapped,
input logic table_overflow,
// Joined -- none of these is computable by any one component.
output logic [15:0] checked_closure_pct,
output logic [15:0] octet_weighted_pct,
output logic [31:0] bare_closure_cells,
// Quality -- the error bar on the three above.
output logic [15:0] join_coverage_pct,
output logic [15:0] ambiguous_pct_x10,
output logic join_untrustworthy,
// Width -- Section 6's constants, reported rather than assumed.
output logic [7:0] beat_octets,
output logic [7:0] offsets_reachable,
output logic dual_dimension_live
);
localparam int W = octets_of(WCLASS);
always_comb begin
// The chapter's headline. Of the cells this run closed, what share
// were closed by a frame a scoreboard actually compared?
checked_closure_pct = (c_first_hit == 32'd0) ? 16'd0
: 16'((c_first_hit_checked * 32'd100) / c_first_hit);
// The same, weighted by how much of each frame the comparison
// covered -- Chapter 20.3 Section 2's invariant core share.
octet_weighted_pct = (c_wire_octets == 32'd0) ? 16'd0
: 16'((32'(checked_closure_pct) * c_core_octets) / c_wire_octets);
bare_closure_cells = c_first_hit_bare;
// What share of planned frames produced a joined row at all.
join_coverage_pct = (c_rows_closed == 32'd0) ? 16'd0
: 16'(((c_rows_closed - c_orphan_plan) * 32'd100) / c_rows_closed);
ambiguous_pct_x10 = (c_observed == 32'd0) ? 16'd0
: 16'((c_ambiguous * 32'd1000) / c_observed);
// Section 3 and Section 13's two silent corruptions, plus the one
// from Section 4. Any of the three and the numbers above are a
// mixture of frames rather than a measurement of one.
join_untrustworthy = uid_wrapped || table_overflow ||
(ambiguous_pct_x10 > 16'd50);
beat_octets = 8'(W);
offsets_reachable = (W < 4) ? 8'd1 : 8'(W / 4);
dual_dimension_live = (W > 8'd20);
end
endmoduleClassification: a reporting block whose first three outputs did not exist before this chapter and whose last three are a build's own parameters, printed.
What it teaches: that join_coverage_pct has to be read before checked_closure_pct, and that the order matters more than either number. A run reporting 7% checked closure over a join that covers 40% of its traffic has said almost nothing; the same 7% over a 98% join is a finding. Chapter 20.3 §14 made the same argument about octet_coverage_pct and Chapter 20.4 §19 about the three denominators — and this is the third time in one module that a percentage needed its own denominator reported alongside it.
And it teaches why the width constants are outputs at all. They are not measurements; they are the build's parameters printed into the same log as the results — so that a report from a 10 Gb/s run and one from a 100 Gb/s run cannot be compared by accident. dual_dimension_live reading 0 next to a coverage figure is the fastest available explanation of why that figure is 128× smaller than the last one.
Deliberately simplified: octet_weighted_pct multiplies a cell share by an octet share, which are different kinds of quantity — the product is a composite figure of merit this chapter defines and Section 17 derives, not a percentage of anything that exists. join_untrustworthy folds three unrelated failures into one bit where the responses differ: a wrap needs a wider uid, an overflow needs a deeper table, an ambiguous rate needs a stimulus change. And ambiguous_pct_x10's threshold of 5.0% is a judgement, where Section 4 showed the real value on control traffic is 63.1%.
Production implication: bare_closure_cells is the output to put on the front page of a closure report. It is a count, not a percentage — deliberately, because it is the number of coverage cells that were closed by a frame nothing checked — and a count is what a team can work through. Chapter 20.4 §18 argued that closure is evidence of stimulus rather than of correctness; this counter is that argument with a list attached, and a closure sign-off that does not look at it is signing off on the sampler.
16. RTL 8 — The Agent Conformance Monitor
// ---------------------------------------------------------------------
// agent_conformance_monitor -- six verdicts, and FIVE of them fire on
// an agent that is working perfectly.
//
// Section 6 is the reason. Most of what goes wrong with a reusable
// agent is not a bug in the agent; it is a constant somewhere else
// that did not move when the width did, and no other block in the
// environment is in a position to notice.
// ---------------------------------------------------------------------
module agent_conformance_monitor
import uvmagent_pkg::*;
#(
parameter width_class_e WCLASS = W_CGMII,
parameter int SPILL_TARGET = 23,
parameter int MIN_JOIN_PCT = 90
)(
input logic clk,
input logic rst_n,
input logic [31:0] c_rows_closed,
input logic [15:0] join_coverage_pct,
input logic [15:0] ambiguous_pct_x10,
input logic [7:0] phases_seen,
input logic [7:0] residues_seen,
input logic uid_wrapped,
input logic table_overflow,
input logic uid_scope_shared,
output logic spill_target_impossible,
output logic phase_space_stale,
output logic residue_space_stale,
output logic join_thin,
output logic identifier_corrupt,
output logic agent_inert,
output logic agent_sound
);
localparam int W = octets_of(WCLASS);
localparam int LEN_SPAN = 1455;
localparam int SPILL_SET = (W <= 1) ? 0 : (LEN_SPAN / W);
localparam int OFF_REACH = (W < 4) ? 1 : (W / 4);
// Section 6's row ten, checked at elaboration against THIS width.
assign spill_target_impossible = (SPILL_SET <= SPILL_TARGET);
// A run that has seen more phases or residues than this width HAS is
// a constant from another build still in the environment.
assign phase_space_stale = (phases_seen > 8'(OFF_REACH));
assign residue_space_stale = (residues_seen > 8'(W));
assign join_thin = (c_rows_closed > 32'd10000) &&
(join_coverage_pct < 16'(MIN_JOIN_PCT));
assign identifier_corrupt = uid_wrapped || table_overflow ||
uid_scope_shared;
assign agent_inert = (c_rows_closed == 32'd0) &&
(ambiguous_pct_x10 == 16'd0);
assign agent_sound = !spill_target_impossible && !phase_space_stale &&
!residue_space_stale && !join_thin &&
!identifier_corrupt && !agent_inert;
always_ff @(posedge clk) begin
if (rst_n && spill_target_impossible)
$display("[agent] spill target %0d unreachable at W=%0d (set size %0d)",
SPILL_TARGET, W, SPILL_SET);
end
endmoduleClassification: an elaboration-time auditor wearing a run-time interface.
What it teaches: that spill_target_impossible is decidable before the clock starts and is therefore the cheapest verdict in Module 20. It compares a parameter against a set size computed from another parameter. No stimulus, no run, no waveform — and it catches the failure Section 12's table describes at three of the four widths. Chapter 20.2 §10's silent bind failure had the same character: a defect that is a fact about the build rather than about the behaviour, found in milliseconds or in a week.
And it teaches what phase_space_stale is really testing. It asserts that the environment has not observed more beat phases than this width can produce — which sounds like a tautology and is not, because the counter that reports phases_seen belongs to Chapter 20.1 §15 and is sized for 64. A 10 Gb/s build with a 100 Gb/s generator telemetry block attached will report up to 64 phases on an interface that has 2, and this is the one comparison in the environment that can see both numbers.
Deliberately simplified: SPILL_SET is LEN_SPAN / W, which is the right count to within one and is not exact — the true counts are 182, 23 and 11 against this estimate's 181, 22 and 11. The estimate is conservative in the right direction at every width in the family, which is why it is acceptable, and it would not be if the comparison were an equality. uid_scope_shared is an input rather than a derivation, because Section 3's frame_uid_t has no scope field for the monitor to check. And five of the six verdicts are combinational over run-time counters that only settle at the end of a run.
Production implication: agent_sound is the bit to gate a regression's coverage merge on, and the reason is that five of its six terms are about numbers being comparable rather than about the design being right. Merging a 10 Gb/s run's coverage database into a 100 Gb/s one produces a union over two different cross spaces — 6 720 cells and 860 160 — and the merged percentage is against whichever denominator the tool saw last. One bit, checked before the merge, is cheaper than the meeting.
17. The Joined Number: 7.4% Behind a 100% Report
Everything in this chapter exists so that this section's arithmetic can be done. It is one worked run and the conclusion is a single number.
The run: a loopback regression at 100 Gb/s that has closed everything it can reach.
| Cells | |
|---|---|
| declared — Chapter 20.4 §4 | 860 160 |
| legal | 467 328 |
| reachable in a loopback | 116 832 |
| covered | 116 832 |
| the tool's report | 100% |
Now split the closed cells by their error-class coordinate, which is the dimension that decides whether a scoreboard can compare the frame at all.
| Error class | Cells | Share | Scoreboard's check |
|---|---|---|---|
| none | 36 864 | 31.55% | an octet-for-octet comparison |
| CRC error | — | — | a negative expectation |
| alignment error | — | — | a negative expectation |
| undersize | — | — | a negative expectation |
| oversize | — | — | a negative expectation |
| all four error classes | 79 968 | 68.45% | "it did not arrive" |
Row six is the first half of the answer. Chapter 7.3 says the design discards a frame that fails its check sequence, is not whole octets, or is outside the size limits — so the frame never reaches the scoreboard's receive side and there is nothing to compare. Chapter 20.3 §8 handles this correctly: the expectation is c_bad_fcs_dropped and an arrival would be the failure. That is a real check and it is a one-bit one. Sixty-eight per cent of this run's closure rests on it.
The second half is Chapter 20.3 §2's invariant core, which bounds what the remaining 31.55% is worth.
| Traffic | Core share | Cells × core |
|---|---|---|
| 64 octets, one payload octet | 23.4% | 7.38% |
| 64 octets, 46 payload octets | 93.8% | 29.60% |
| 1 518 octets | 99.7% | 31.46% |
So the composite is between 7.4% and 31.5%, and which end a given regression sits at is decided by its payload lengths and nothing else.
A loopback regression reporting 100% coverage has, on control traffic, compared the contents of the frames behind 7.4% of it. On maximum-size traffic the same report is worth 31.5%.
Three things that number is not.
| Not | |
|---|---|
| a criticism of the coverage model | Chapter 20.4's 116 832 is correct |
| a criticism of the scoreboard | Chapter 20.3's negative expectations are correct |
| a number any one component could report | it needs all four, joined on Section 3's uid |
Row three is the point. The generator knows what it produced, the scoreboard knows what it compared, the assertion library knows what fired and the coverage model knows what closed — and the product of a cell share and an octet share is a statement about the same frames, which is only a statement once the frames are the same frames.
And the 1.9 percentage points between 29.60% and 31.46% are worth noticing too, because they are the entire benefit of running maximum-size traffic instead of full-payload minimum-size traffic — whereas the gap from 7.38% to 29.60% is 22.2 points and costs 45 octets of payload. The cheap change is much larger than the expensive one, and no report in Module 20 could say so before this chapter.
18. What the Agent Assumes
Nine assumptions. Four are about the identifier, three about the width and two about the components the agent joins — and the ones most likely to be false are not the ones about the width.
| Assumption | From | If false | |
|---|---|---|---|
| 1 | the digest is unique among live frames | the stimulus | Section 4 — 63.1% ambiguous on control traffic |
| 2 | the uid does not wrap within a run | 24 bits, 112 ms | rows from two frames merge silently |
| 3 | only one agent mints into this uid space | convention | Section 14's row two — wrong rows, no flag |
| 4 | the frame is not modified to carry the key | Section 2 | the experiment changed |
| 5 | BEAT_OCTETS is the only width in the environment | Section 5 | Section 6's eleven constants disagree |
| 6 | the gap is chosen in octets, not beats | Chapter 19.3 §6 | every phase number moves — Section 11 |
| 7 | the wire period is a multiple of four | Chapter 19.3 §6's deficit | offsets_reachable is W, not W/4 |
| 8 | the scoreboard's verdict arrives before retirement | Chapter 20.3 §11 | c_first_hit_checked reads zero |
| 9 | the coverage sampler's first-hit bit is per frame | Chapter 20.4 §9 | the join's numerator counts cells, not frames |
Row one is the assumption most likely to be false and it is not about the agent at all. Chapter 20.3 §3's digest is a function of the invariant core, and on the control traffic every Ethernet environment runs a lot of, that core is fifteen octets of which fourteen are constant. The agent's join key has eight bits of entropy on exactly the traffic where the scoreboard's comparison is also weakest — the same cause, in two components, and neither could have seen it alone.
Row seven is the one that is false the moment somebody attaches this agent to a design that is not Chapter 19.3's. The W/4 in Section 5 is not a property of Ethernet; it is a property of a transmit assembler that chooses its gap to lane-align the next frame. A design that emits a constant twelve-octet gap has periods of every residue mod 4 — so all W offsets become reachable, the loopback's 25.0% becomes 100%, and Chapter 20.4's reachable denominator is four times what the agent reports. The assumption is cited, the citation is to one chapter, and the chapter is about one design.
Row nine is the subtlest and it is about a type rather than a value. Chapter 20.4's sampler fires on a cell; the join counts frames. They agree only because a frame fills exactly one cell — which is true for this model and would stop being true the moment a dimension is added that a single frame can occupy two values of. The join would then count one frame twice and checked_closure_pct would exceed 100%, which is at least a visible failure.
And three things deliberately not assumed:
| Not assumed | Why not |
|---|---|
| that a missing observation means a lost frame | Chapter 20.3 §13's five legal drops |
| that the four components agree | the disagreement is the measurement — Section 17 |
| that the agent is bound | agent_inert — Chapter 20.2 §10's failure from the other side |
Row two is this chapter's central discipline and it is the opposite of the instinct. A join is usually built to confirm that independent components agree. This one is built because they do not, and because the gap between "a cell was sampled" and "a frame was compared" is 68.45% of a closed run — a disagreement that is entirely correct on both sides.
19. The Cost, Accounted
The agent is by an order of magnitude the most expensive structure Module 20 has built, and all of the cost is in one place.
| Block | Flops | Nature |
|---|---|---|
uvmagent_pkg | 0 | types |
uid_allocator — 256 entries × 57 bits | ~14 721 | the identifier table |
beat_width_adapter | 0 | combinational |
agent_monitor | ~96 | counters |
agent_sequence_layer | ~64 | an LFSR and a counter |
agent_driver | ~90 | beat shaping |
observation_join — 1 024 rows × 72 bits | ~74 017 | the join table |
agent_telemetry | 0 | combinational |
agent_conformance_monitor | 0 | combinational |
| total | ~88 988 flops |
Two blocks are 99.7% of it and both are tables, which is the shape of every structure whose job is to remember something per frame rather than per check.
Put that against the module and the design.
| Flops | |
|---|---|
| Chapter 20.1 — the generator | ~1 050 |
| Chapter 20.2 — the assertion library | ~730 |
| Chapter 20.3 — the scoreboards | ~1 760 |
| Chapter 20.4 — the coverage model | ~650 |
| Chapter 20.5 — the injector | ~990 |
| this chapter — the agent | ~88 988 |
| Module 20 total | ~94 168 |
| Module 19's datapath, all of which ships | ~14 166 |
The verification environment is 6.6× the design it verifies, and one table is 78.6% of the environment. Which looks like a scandal and is not, because the unit is wrong.
| Read as | Size | Verdict |
|---|---|---|
| flip-flops | 88 988 | absurd — six MACs' worth |
| memory | 10.86 KiB | trivial — a simulator allocates it and forgets |
| against Chapter 19.5 §3's receive FIFO | 33.9% of 32 KiB | a third of one buffer |
That is the whole argument for why the join lives in a testbench and not in the design. As gates it is unaffordable; as an associative array in a simulator it is eleven kilobytes and nobody notices — and the reason nobody has built this measurement into a product is that the product would have to carry 89 000 flops to compute a number only a verification engineer reads.
And the sizing is not arbitrary. Both tables are bounded by frames in flight, and that is a number Module 19 already derived.
| Table | Depth | Bound |
|---|---|---|
uid_allocator | 256 | live digests — Chapter 19.1 §6's 12 in the pipeline, plus the FIFO's |
observation_join | 1 024 | Chapter 20.3 §11's 631-cycle verdict latency, at 1.312 cycles per frame |
Row two is derivable rather than chosen. At minimum size a frame arrives every 1.312 cycles, and Chapter 20.3 §11's scoreboard verdict is 631 cycles behind it, so 481 frames are awaiting a verdict at any moment and 1 024 rows is a factor of 2.1. At the window's actual setting of 100 000 cycles it is 76 220 frames and 1 024 rows overflow in 6.9 microseconds — which is table_overflow, and it is the difference between a bound that was derived and a bound that was guessed.
Module 20's stimulus, checking and measurement, finally:
| Flops | Share | |
|---|---|---|
| stimulus — Chapter 20.1 and Chapter 20.5 | ~2 040 | 2.2% |
| checking — Chapter 20.2 and Chapter 20.3 | ~2 490 | 2.6% |
| measurement — Chapter 20.4 | ~650 | 0.7% |
| identity — this chapter | ~88 988 | 94.5% |
Knowing which frame you are looking at costs more than producing it, checking it and counting it put together, by a factor of 17.2. That is the module's closing number and it is the reason no environment in the track had one before now.
20. Properties Worth Asserting, and One Worth Refusing
Thirty-three properties and eight covers. They divide into four groups, and the group boundaries are the agent's own structure: the identifier, the width, the join, and the numbers the join produces.
Group one — the identifier.
// The uid is a counter and nothing else. If it ever means something,
// every property below it is about a different object.
p_uid_monotonic: assert property (@(posedge clk) disable iff (!rst_n)
minted_valid |=> (minted_uid == $past(minted_uid) + 24'd1));
p_uid_unique_live: assert property (@(posedge clk) disable iff (!rst_n)
mint_req |-> !(r_live[slot_of(minted_uid)] &&
r_uid[slot_of(minted_uid)] != minted_uid));
p_no_row_on_ambig: assert property (@(posedge clk) disable iff (!rst_n)
lookup_ambiguous |-> !obs_valid);
p_obs_uid_matches: assert property (@(posedge clk) disable iff (!rst_n)
obs_valid |-> (r_uid[slot_of(obs.uid)] == obs.uid));
p_wrap_is_terminal: assert property (@(posedge clk) disable iff (!rst_n)
uid_wrapped |=> always uid_wrapped);
p_orphan_accounted: assert property (@(posedge clk) disable iff (!rst_n)
(lookup_req && !lookup_hit) |=>
(c_unattributed == $past(c_unattributed) + 32'd1));
p_digest_stable: assert property (@(posedge clk) disable iff (!rst_n)
commit_valid |-> (commit_digest == $past(commit_digest, 0)));
p_passive_no_mint: assert property (@(posedge clk) disable iff (!rst_n)
(!IS_ACTIVE) |-> !mint_req);Group two — the width. Every property here names W and is true at every value of it; Section 21's rejected property is what happens when one of them is written without naming it.
// Section 5: beats are a function of octets and the width, and the
// relationship is exact rather than approximate.
p_beats_exact: assert property (@(posedge clk) disable iff (!rst_n)
frame_valid |-> (beats_occupied == ((wire_len + W - 1) / W)));
p_offset_in_range: assert property (@(posedge clk) disable iff (!rst_n)
frame_valid |-> (start_offset < 8'(W)));
p_offset_quad: assert property (@(posedge clk) disable iff (!rst_n)
frame_valid |-> ((start_offset % 8'd4) == 8'd0));
p_reach_is_quarter: assert property (@(posedge clk) disable iff (!rst_n)
(W >= 4) |-> (offsets_reachable == 8'(W / 4)));
p_residue_range: assert property (@(posedge clk) disable iff (!rst_n)
frame_valid |-> (residue < 8'(W)));
p_dual_needs_width: assert property (@(posedge clk) disable iff (!rst_n)
spans_two_beats |-> (8'(W) > 8'd17));
p_gap_in_octets: assert property (@(posedge clk) disable iff (!rst_n)
frame_end |=> (gap_octets >= 8'd9 && gap_octets <= 8'd12));
p_degenerate_flag: assert property (@(posedge clk) disable iff (!rst_n)
(phases_this_size == 8'd1) |-> size_is_degenerate);
p_period_mod4: assert property (@(posedge clk) disable iff (!rst_n)
frame_valid |-> ((period % 16'd4) == 16'd0));Group three — the join.
// A row's life is planned, observed, retired. Each transition has a
// precondition and skipping one silently drops it from a denominator.
p_plan_before_obs: assert property (@(posedge clk) disable iff (!rst_n)
obs_valid |-> r_plan[slot_of(obs.uid)]);
p_retire_once: assert property (@(posedge clk) disable iff (!rst_n)
(retire_valid && r_live[slot_of(retire_uid)]) |=>
!r_live[slot_of(retire_uid)]);
p_closed_is_sum: assert property (@(posedge clk) disable iff (!rst_n)
$stable(c_rows_closed) ||
(c_rows_closed == $past(c_rows_closed) + 32'd1));
p_hit_implies_row: assert property (@(posedge clk) disable iff (!rst_n)
(c_first_hit > 32'd0) |-> (c_rows_closed > 32'd0));
p_checked_le_hit: assert property (@(posedge clk) disable iff (!rst_n)
c_first_hit_checked <= c_first_hit);
p_fired_le_hit: assert property (@(posedge clk) disable iff (!rst_n)
c_first_hit_fired <= c_first_hit);
p_bare_consistent: assert property (@(posedge clk) disable iff (!rst_n)
(c_first_hit_bare + c_first_hit_checked) <= c_first_hit +
c_first_hit_fired);
p_overflow_sticky: assert property (@(posedge clk) disable iff (!rst_n)
table_overflow |=> always table_overflow);
p_no_join_on_evict: assert property (@(posedge clk) disable iff (!rst_n)
(plan_valid && r_live[slot_of(plan.uid)]) |-> table_overflow);Group four — the numbers. These are the properties that keep Section 17's percentage from becoming Chapter 20.4 §20's class 91.
p_pct_bounded: assert property (@(posedge clk) disable iff (!rst_n)
checked_closure_pct <= 16'd100);
p_pct_needs_denom: assert property (@(posedge clk) disable iff (!rst_n)
(checked_closure_pct > 16'd0) |-> (c_first_hit > 32'd0));
p_join_pct_bounded: assert property (@(posedge clk) disable iff (!rst_n)
join_coverage_pct <= 16'd100);
p_untrust_implies: assert property (@(posedge clk) disable iff (!rst_n)
(uid_wrapped || table_overflow) |-> join_untrustworthy);
p_width_reported: assert property (@(posedge clk) disable iff (!rst_n)
(beat_octets == 8'(W)));
p_dual_reported: assert property (@(posedge clk) disable iff (!rst_n)
dual_dimension_live == (8'(W) > 8'd20));
p_spill_verdict: assert property (@(posedge clk) disable iff (!rst_n)
(SPILL_SET <= SPILL_TARGET) |-> spill_target_impossible);
p_sound_is_conj: assert property (@(posedge clk) disable iff (!rst_n)
agent_sound |-> (!join_thin && !identifier_corrupt));
p_inert_is_loud: assert property (@(posedge clk) disable iff (!rst_n)
(c_rows_closed == 32'd0 && c_observed > 32'd1000) |->
!agent_sound);And eight covers, because six of these cases are what a reviewer wants evidence of rather than assurance about.
c_ambiguous_seen: cover property (@(posedge clk) lookup_ambiguous);
c_orphan_obs_seen: cover property (@(posedge clk) lookup_req && !lookup_hit);
c_bare_cell: cover property (@(posedge clk) c_first_hit_bare > 32'd0);
c_all_16_offsets: cover property (@(posedge clk) offsets_seen == 16'hFFFF);
c_degenerate_size: cover property (@(posedge clk) size_is_degenerate);
c_spill_size: cover property (@(posedge clk) residue == 8'(W - 1));
c_dual_beat: cover property (@(posedge clk) spans_two_beats);
c_join_over_95: cover property (@(posedge clk) join_coverage_pct >= 16'd95);21. Verification Scenarios
Fifty-eight scenarios for a component whose output is a number, plus a five-run directed test whose whole content is a width change.
The uid allocator — 10 scenarios.
| # | Scenario | Expected |
|---|---|---|
| 1 | one mint | uid 0, minted_valid |
| 2 | 1 000 mints | c_minted = 1 000, uids 0 to 999 |
| 3 | a lookup on a minted digest | lookup_hit, the right uid |
| 4 | a lookup on an unminted digest | no hit; not an error |
| 5 | two mints with the same digest | c_collisions = 1 |
| 6 | a lookup after that | lookup_ambiguous |
| 7 | 16 777 216 mints | uid_wrapped; the run is void |
| 8 | 257 mints, then a lookup on the first | the entry was overwritten — a miss |
| 9 | a mint with TABLE_DEPTH = 1 | every lookup after one mint misses |
| 10 | reset mid-run | all valid bits clear; uids restart at 0 |
The beat-width adapter — 12 scenarios.
| # | Scenario | Expected |
|---|---|---|
| 11 | W = 64, wire_len = 64 | 1 beat; residue 60 |
| 12 | W = 64, wire_len = 1 475 | 24 beats; residue 63 — the spill size |
| 13 | W = 8, wire_len = 64 | 8 beats; residue 4 |
| 14 | W = 1, wire_len = 64 | 64 beats; residue 0 always |
| 15 | W = 64, start octet 37 | offset 37 — legal in the type, unreachable in the design |
| 16 | the same, driven by Section 11 | offset is always a multiple of 4 |
| 17 | W = 64, wire_len = 1 518 | phases_this_size = 1, size_is_degenerate |
| 18 | W = 64, wire_len = 1 520 | phases_this_size = 16 |
| 19 | W = 8, wire_len = 1 518 | phases_this_size = 1 of 2 reachable |
| 20 | W = 128, wire_len = 1 518 | still 1 — period 1 536 divides 128 |
| 21 | W = 8, any size | spans_two_beats never asserts |
| 22 | W = 64, minimum size | spans_two_beats asserts |
The sequence layer — 9 scenarios.
| # | Scenario | Expected |
|---|---|---|
| 23 | w_spill = 0 | lengths uniform; spill sizes at chance |
| 24 | w_spill = 50, spill_modulus = 64 | half the frames at L ≡ 3 (mod 64) |
| 25 | the same with spill_modulus = 8 | half at L ≡ 3 (mod 8) — a different set |
| 26 | spill_modulus = 0 | spill_target_impossible; no adjustment |
| 27 | W = 128 with the 23 target | spill_target_impossible at elaboration |
| 28 | allow_illegal clear | no frame below 64 — Chapter 19.7 §7 unreachable |
| 29 | allow_illegal set, w_runt = 5 | runts in 5 to 47 only |
| 30 | grep the block for "beat" | no match — the reuse claim, checked |
| 31 | the same items at three widths | byte-identical item streams |
The driver — 8 scenarios.
| # | Scenario | Expected |
|---|---|---|
| 32 | W = 64, L = 64 | 1 data beat, then an idle beat |
| 33 | W = 64, L = 65 | 2 data beats; valid_octets = 1 on the last |
| 34 | W = 8, L = 65 | 9 beats; valid_octets = 1 |
| 35 | the gap counter held in beats | period becomes L + 72 — every phase moves |
| 36 | the gap counter held in octets | period L + 20 − (L mod 4) |
| 37 | 1 000 frames at L = 1 518 | every frame starts at offset 0 |
| 38 | 1 000 frames at L = 1 520 | all 16 reachable offsets |
| 39 | a passive build | pull never asserts |
The join — 11 scenarios.
| # | Scenario | Expected |
|---|---|---|
| 40 | plan, observe, retire, in order | one closed row |
| 41 | observe with no plan | c_orphan_obs |
| 42 | plan, retire, no observe | c_orphan_plan |
| 43 | 1 025 plans without retirement | table_overflow |
| 44 | two uids 1 024 apart | the same slot — an eviction |
| 45 | a retire on a stale uid | ignored; no counter moves |
| 46 | a first-hit frame that is compared | c_first_hit_checked |
| 47 | a first-hit frame that is discarded | c_first_hit_bare |
| 48 | a scoreboard verdict 631 cycles late | joined — the row is still live |
| 49 | a verdict 100 000 cycles late | the row was evicted at 1 024 |
| 50 | all rows retired | join_coverage_pct = 100 |
The joined numbers — 8 scenarios.
| # | Scenario | Expected |
|---|---|---|
| 51 | a loopback run, control traffic | octet_weighted_pct ≈ 7 |
| 52 | the same with 46-octet payloads | ≈ 30 |
| 53 | the same at 1 518 octets | ≈ 31 |
| 54 | a run with the injector armed | c_first_hit_bare climbs |
| 55 | a 40% orphan rate | join_thin; the numbers are void |
| 56 | uid_wrapped | join_untrustworthy, identifier_corrupt |
| 57 | a 10 Gb/s build's telemetry at 100 Gb/s | phase_space_stale |
| 58 | merging a W = 8 database into W = 64 | agent_sound clear before the merge |
And the directed test, because random stimulus will not produce it.
The case: the same agent, the same sequence layer, the same seed, at four widths — and the assertion that survives every one of them and fails between two.
No constrained-random generator produces this, because the width is not a field of any item. It is a parameter, and a parameter does not vary within a run. The only way to reach the case is to run the environment four times and then a fifth time with two widths present at once.
| Run | W | What it establishes |
|---|---|---|
| A | 1 — GMII | 3 of 6 coverage dimensions are degenerate |
| B | 8 — 10 Gb/s | p_offset_preserved passes |
| C | 64 — CGMII | p_offset_preserved passes |
| D | 128 — projection | p_offset_preserved passes |
| E | 64 and 8, across a converter | p_offset_preserved FAILS on 87.5% of offsets |
The oracle is four-part, and each part is checkable independently.
| Part | Run A | Run B | Run C | Run D | Run E |
|---|---|---|---|---|---|
| declared cross | 105 | 6 720 | 860 160 | 3 440 640 | both, and incomparable |
| reachable offsets | 1 | 2 | 16 | 32 | 16 and 2 |
p_offset_preserved | passes | passes | passes | passes | fails, 87.5% |
| the item stream | identical | identical | identical | identical | identical |
Row four is the control and it is the reason the test is conclusive. The sequence layer emitted the same items in all five runs — same seed, same lengths, same tags — so nothing about the stimulus changed. Row three's four passes and one failure are entirely a consequence of where the property was bound, and rows one and two say the environment measuring it was a different size each time.
Run A is worth its own sentence. At GMII there is no residue dimension, no offset dimension and no dual-frame dimension: the cross is 105 cells and closes in a few thousand frames. A team that validates the agent at 1 Gb/s and ships it has validated three of the six dimensions out of existence and will report closure in an afternoon.
And run E is the only run in the table that needs two agents, which is why it is the one nobody runs: it requires the uid space to be shared between them, which Section 14's row two prohibits — so reaching the case at all requires deliberately doing the thing the chapter says never to do, under a scope field the package does not have. That is the honest reason this class of bug survives: the configuration that exposes it is the configuration the methodology forbids.
22. Debugging a Reusable Agent
Six complaints, in the order they arrive.
Complaint 1 — "the join produces no rows."
| Check | If yes | Meaning |
|---|---|---|
c_observed moving? | no | the monitor is not bound — Chapter 20.2 §10 |
c_unattributed = c_observed? | yes | a passive agent, working correctly |
ambiguous_pct_x10 high? | yes | Section 4 — the stimulus, not the digest |
table_overflow? | yes | rows evicted before their verdict arrived |
Row two is the row that wastes a day. A passive agent produces no joined rows at all and this is correct — it never minted the frames it is watching. The evidence that it is working is c_observed climbing while c_rows_closed stays at zero, and an environment that treats those two as a contradiction will spend the day looking for a bug in the allocator.
Complaint 2 — "checked_closure_pct is 7 and that cannot be right."
| Check | If yes | Meaning |
|---|---|---|
| is the traffic control frames? | yes | Section 17 — 23.4% core, 31.55% non-error cells |
join_coverage_pct above 90? | yes | the 7 is real |
c_first_hit_bare large? | yes | cells closed by discarded frames |
| is the injector armed? | yes | 68% of the cross is error classes |
All four rows can be yes at once and nothing is wrong. Seven per cent is the correct answer for a loopback regression of control traffic, and the useful response is not to investigate the agent — it is to lengthen the payloads, which Section 17 prices at 22.2 points for 45 octets.
Complaint 3 — "coverage collapsed after we moved to 10 Gb/s."
| Check | If yes | Meaning |
|---|---|---|
beat_octets = 8? | yes | the build is what you think |
dual_dimension_live = 0? | yes | a whole dimension is gone — Section 6 |
| declared cells = 6 720? | yes | 128× smaller, correctly |
| did the percentage rise? | usually | a smaller space closes faster |
Row four is the direction the surprise usually runs in and it is the opposite of the complaint. Moving down in width shrinks the declared space by 128× and the reported percentage goes up, which reads as an improvement. The collapse the team is seeing is in the cell count, not the percentage — and the two numbers move in opposite directions across the same change.
Complaint 4 — "the spill list never completes."
| Check | If yes | Meaning |
|---|---|---|
spill_target_impossible? | yes | the answer, at elaboration |
what is beat_octets? | 64 | the set has exactly 23 members |
is the target > 23? | yes | a strict inequality over a 23-member set |
at W = 128? | 11 members | impossible by twelve |
Row three is the finding and it is not a width problem. c_spill_sizes > 23 was never reachable at any width, including the one it was written for — the set has 23 members and the test demands 24. Section 6 found it by generalising a constant nobody had a reason to look at; the monitor reports it in a $display at time zero.
Complaint 5 — "the same frame has two different offsets."
| Check | If yes | Meaning |
|---|---|---|
| are the two monitors at different widths? | yes | Section 20's rejected property |
is obs_sys.offset = obs_wire.offset mod 8? | yes | both are right; the property is wrong |
is obs_wire.offset under 8? | no | which is why it fails on 87.5% of offsets |
| does it pass at 1 518? | yes | offset 0 forever — Chapter 20.1 §17 |
Row four is the reason this survives review. The property fails on 87.5% of the reachable offsets and passes on every frame of the stress test everybody runs, because a 1 518-octet stream visits offset 0 and only offset 0. A defect that is present on seven-eighths of the space and absent from the regression is the hardest shape there is.
Complaint 6 — "two runs' coverage merged to less than either."
| Check | If yes | Meaning |
|---|---|---|
different beat_octets? | yes | two different cross spaces |
was agent_sound checked? | no | Section 16 exists for this |
| are the bin names the same? | yes | which is why the tool merged them |
| is the union meaningful? | no | a cell named offset_9 exists in one and not the other |
Row three is the mechanism. A coverage tool merges on bin names, and the bin names do not carry the width — so offset_9 from a 64-octet build and the absence of offset_9 from an 8-octet build merge into a hole that no run can ever fill. The merged database is not wrong about any run; it is a statement about a space that does not exist.
And the three symptoms this chapter is systematically blamed for:
| Symptom | Blamed on | Usually is |
|---|---|---|
| a low checked-closure percentage | the scoreboard | the error-class share — Section 17 |
| a coverage number that moved 128× | a regression | the beat width — Section 8 |
| a property that passes and should not | the design | a constant with two values — Section 20 |
23. Misconceptions
Misconception 1 — "an agent is a container for the driver and the monitor."
The wrong model: an agent is an organisational convenience — somewhere to put the components that touch one interface, so the environment's class hierarchy looks tidy.
What it costs: an agent with no invariant is an agent that cannot be wrong, which means it cannot be right either. Every failure in Sections 13 and 14 — a shared uid space, an evicted row, an ambiguous key — is a violation of something a container does not have. Teams with tidy agents and four unjoinable numbers are the normal case, and Chapter 20.4 §26 described exactly that state.
The corrected model: an agent is the scope of a frame identifier. Every frame it produces or observes has exactly one uid, every observation about that frame carries it, and the agent's boundary is the boundary of that uid space. Everything else in it follows from that. Section 2.
Misconception 2 — "the sequence layer is width-independent, so the agent is."
The wrong model: the reuse claim is about the sequence layer, the sequence layer really does not name a beat, so the agent ports across the family for free.
What it costs: the sequence layer is 64 flops of the agent's 88 988 and none of the five derived numbers that move with the width live inside it. They live in Chapter 20.4's cross, Chapter 20.1 §15's thresholds and Chapter 20.1 §17's gcd — three other chapters, four other files. A port that touches the agent and nothing else leaves a coverage model measuring 6 720 cells of a space that has 860 160.
The corrected model: the sequence layer's independence is real and is the cheap half. The expensive half is eleven constants in four files, five of which move — Section 6 lists them and Section 10 prices them.
Misconception 3 — "a wider digest makes the join reliable."
The wrong model: ambiguous lookups are hash collisions, hash collisions are fixed by more bits, so widen the digest.
What it costs: on control traffic the digest's input has eight bits of entropy — fourteen constant header octets and one payload octet — so a 64-bit digest has exactly the same 63.1% ambiguity rate as a 32-bit one, at twice the comparator cost. The change is measurable in area and invisible in the metric.
The corrected model: the collision rate is a property of the stimulus. Varying one payload octet moves the rate from certainty to 0.000 76%; widening the hash moves it by nothing. Section 4, and the ratio between the two responses is about eighty thousand.
Misconception 4 — "coverage went up when we moved to 10 Gb/s, so the environment got better."
The wrong model: the same tests, the same design, a higher closure percentage — progress.
What it costs: the declared cross at 8 octets per beat is 6 720 cells against 860 160, because two of the six dimensions are the beat width and a third disappears below 20 octets. A percentage against a 128× smaller denominator is not comparable to the one before it, and a team that reports the two on one chart has drawn a trend through two different spaces.
The corrected model: report beat_octets and dual_dimension_live beside every coverage figure — Section 15 — so that two runs at two widths cannot be compared by accident. The width-invariant number is the loopback's 25.0% of legal, and it is invariant because W/4 divided by W does not depend on W.
Misconception 5 — "the property passes at every width we support, so it is correct."
The wrong model: a parameterised property checked at every value of its parameter has been exhaustively verified over its configuration space.
What it costs: Section 20's class 93. The configuration space's members are not the whole space — a width converter is a point where the parameter has two values at once, and no member of the space describes it. The property passes on 12.5% of the reachable offsets there and fails on 87.5%, and passes always on the 1 518-octet stress test.
The corrected model: a property that crosses a configuration boundary must name both configurations or neither. obs_sys.offset == obs_wire.offset % W_SYS names both; obs_wire.wire_len == obs_sys.wire_len names neither and is the invariant the tempting property was reaching for.
Misconception 6 — "a closed coverage model means the design was checked."
The wrong model: 116 832 of 116 832 cells covered, no mismatches, sign off.
What it costs: 68.45% of those cells are error-class cells, where Chapter 7.3 requires the frame to be discarded and the scoreboard's entire check is "it did not arrive." The remaining 31.55% are compared against Chapter 20.3 §2's invariant core, which on control traffic is 23.4% of the frame. The composite is 7.4% — and no component in Module 20 could have said so.
The corrected model: closure is evidence about the stimulus. What was checked is a separate measurement and it needs a join — Section 17 — and c_first_hit_bare is the count a sign-off should look at rather than the percentage.
24. Interview Questions
Question 1 — "Why does a verification environment need an agent at all?"
What the answer should establish: that the agent is the scope of a frame identifier, and that without one the environment's components produce numbers that cannot be combined. A strong answer gives the concrete failure: four components, four keys — a run-level tally, a checker instance, a twelve-deep recycled tag and a coverage cell — so "of the cells we closed, how many were behind a frame anybody compared?" is not a question the environment can parse. A weak answer says "reuse" and stops, which is true and is the smaller half.
Question 2 — "Your agent's monitor is passive. How does it know which frame it is looking at?"
What the answer should establish: that it cannot be told and must derive it. A passive agent has no driver and no item, so the only key available is a function of the frame's own octets — Chapter 20.3 §3's digest over the invariant core. A strong answer immediately raises the consequence: that key can collide, and the collision rate is set by the stimulus rather than by the hash. The strongest answer names the case: control traffic, 15-octet core, 14 constant octets, 256 distinguishable values.
Question 3 — "What actually changes when you move this agent from 100 Gb/s to 10 Gb/s?"
What the answer should establish: that the agent is the cheap part. Two of eight blocks are entirely the width, three are untouched. The expensive change is outside the agent: Chapter 20.4's cross moves by 128× — 64 from W² and 2 from the dual-frame dimension disappearing — and two of Chapter 20.1 §15's five thresholds move, one of them inversely. A weak answer discusses port widths.
Question 4 — "A property holds at every width your agent supports. Is it correct?"
What the answer should establish: not necessarily, and the gap is the seam. A width converter is a point where the parameter has two values simultaneously, and no member of the configuration space describes it. A strong answer gives the arithmetic: an offset-equality property across a 64-to-8-octet converter holds only when the wire-side offset is below 8, which is 2 of the 16 reachable offsets — 12.5%. The strongest answer adds that it passes always at 1 518 octets, because that stream visits offset 0 forever.
Question 5 — "Your regression reports 100% coverage. What did it check?"
What the answer should establish: two multiplications. 68.45% of the reachable cross is error-class cells where the check is a negative expectation; the remaining 31.55% is compared against an invariant core that is 23.4% of a control frame. 7.4%. A strong answer says what to do about it: lengthen the payloads — 45 octets buys 22.2 points — and look at c_first_hit_bare, which is a count rather than a percentage.
Question 6 — "How big is a frame-identity table, and why is it in the testbench?"
What the answer should establish: that the two tables are 99.7% of the agent and 94.5% of Module 20 — about 89 000 flops. A strong answer flips the unit: as flip-flops that is six MACs' worth and absurd; as memory it is 10.9 KiB, a third of Chapter 19.5 §3's receive FIFO, and free. That is the whole reason the measurement lives in a simulator: a product would have to carry 89 000 flops to compute a number only a verification engineer reads.
25. Questions and Answers
26. What's Next
Module 20 is complete: six chapters, one environment, and one number the environment could not produce until the last of them.
| Chapter | Builds | Its number |
|---|---|---|
| Chapter 20.1 | the generator | five lists, and which weights cannot reach them |
| Chapter 20.2 | the assertion library | the fired percentage over a measured corpus |
| Chapter 20.3 | the scoreboards | 23.4% of a control frame is comparable |
| Chapter 20.4 | the coverage model | 860 160, 467 328, 116 832 — three denominators |
| Chapter 20.5 | the error injector | 14 design paths, and the 3 no frame reaches |
| this chapter | the agent | 7.4% — the first number that needed all five |
Module 21 turns the environment around. Everything in Module 20 asks does this design do what it should; Module 21 asks this link is broken and I do not know why — which is a different problem with a different method, and the first three chapters are the method, the catalogue and the first failure class.
Chapter 21.1 is the method, and it inherits this module's central discipline directly. A debugging procedure is a sequence of measurements that each halve a space, and the reason Chapter 20.4's three denominators and this chapter's join_coverage_pct matter is the same reason a diagnostic step needs to say what it ruled out. Chapter 20.5 §22's six complaint tables are that method applied to one component; Module 21 applies it to a link.
Chapter 21.2 is the error taxonomy, and it is Chapter 7.3's five classes widened to everything a link can do wrong — symbol errors, drops, FIFO overflows, descriptor faults. This chapter's Section 17 split the coverage cross by error class and found 68.45% of it in four classes whose entire check is "it did not arrive"; the taxonomy is where "it did not arrive" gets its causes.
Chapter 21.3 is CRC errors, which Chapter 20.5 §4 priced at 32 XOR gates to inject and one design path to reach. Diagnosing them is the opposite problem: the frame is broken, the counter moved, and the question is whether the fault is in the channel or in the logic — Chapter 19.4 §14's equivalence checker on one side and a marginal cable on the other.
And the series is now ninety-three classes long. Chapter 20.2 §8 sorted the first eighty-eight into six groups; Chapter 20.4 §20's class 91 opened a seventh. Class 93 does not fit any of the seven, because every one of them is about a property being wrong somewhere — and this one is correct at every point of its configuration space and wrong only between two points. A taxonomy indexed on where a property fails has no row for a property that fails nowhere and still fails, which is the second time in three chapters that the taxonomy has needed extending.
Continue learning
Related tutorials
- Related topic
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.
- 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
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
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.
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.
