Skip to content
VLSI Mentor

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.

ComponentIts numberKeyed on
Chapter 20.1 §15's generatorlists_complete — five bitsnothing; a run-level tally
Chapter 20.2 §15's assertion libraryfired_pct_x10the checker instance
Chapter 20.3 §14's scoreboardsoctet_coverage_pctChapter 20.3 §7's tag, which is 12 deep and reused
Chapter 20.4 §19's coverage modelthree percentagesthe 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 widthAt 8 octetsAt 64 octets
Chapter 20.4's declared cross6 720 cells860 160 cells
Chapter 20.1 §9's reachable offsets2 of 816 of 64
Chapter 20.1 §17's spill sizes18223
Chapter 20.1 §15's runt threshold4343

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:

NumberWeakens whenChapter
list_coverage_pcta list is redefined smallerChapter 20.1 §15
fired_pct_x10never-firing checkers are deletedChapter 20.2 §15
octet_coverage_pcta field is waived as "transformed"Chapter 20.3 §14
pct_of_reachablethe environment improvesChapter 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 agentThis chapter's argument
reuse across projectstrue, and Section 10 prices it
a clean driver/monitor splittrue, and it is not the point
a place the frame's identity livesthe 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

Module twenty built four components and each one keys its records on something different. The generator's telemetry keys on nothing at all: it is a run level tally of five bits. The assertion library keys on the checker instance, so its fired percentage is a statement about properties rather than about frames. The scoreboard keys on a tag that is twelve deep and recycled, so a tag names a different frame every twelve frames. The coverage model keys on a cell, which many frames map to. Four keys, no join, and so a reviewer cannot ask of the cells this run closed how many were closed by a frame the scoreboard compared. The agent's answer is to mint one identifier per frame at the sequence layer, before the frame exists on any wire, and to make every observation about that frame carry it. The monitor cannot be told the identifier, because a passive agent has no driver and no item, so it must recover it from a digest of the frame's own invariant core. Once every component's record carries the same identifier, the four separate numbers become columns of one table and a fifth number becomes computable that none of them could produce.20.1's generatorkeys on nothing20.2's librarykeys on the checker20.3's scoreboarda 12-deep recycled tag20.4's modelkeys on the cellOne frame uidminted at the sequenceRecovered bydigest20.3's invariant coreOne row per frameSection 13A fifth number7.4% — Section 1712
Figure 1 — four components, four keys, and one identifier that makes them one row.

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.

QuestionSettled 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.

MechanismWorks?Why not
a sequence number in the payloadnoit changes the frame under test
a reserved EtherTypenoit changes which paths the frame takes
a side-channel from driver to monitornoa passive agent has no driver
a digest of the frame's invariant coreyesChapter 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:

NotBecause
a place to put the scoreboardChapter 20.3's scoreboard is per-path, and an agent is per-interface
a place to put coverageChapter 20.4's cross spans two interfaces
a place to put the checksChapter 20.2 §11's harness binds to the DUT, not to the agent
width-independentSections 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

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ---------------------------------------------------------------------
// 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;

endpackage

Classification: 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.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ---------------------------------------------------------------------
// 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
endmodule

Classification: 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 chooseChapter 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:

FrameCoreShare
64 octets, one payload octet15 octets23.4%
64 octets, 46 payload octets60 octets93.8%
1 518 octets1 514 octets99.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 traffic256
live frames in the table256
expected distinct keys162.0
frames sharing a key with another94 — 36.7%
probability a given lookup is ambiguous63.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 uselessone 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 traffic46 varied payload octets
key space256the digest's 2^32
P(any collision in a full table)certain0.000 76%
expected collisions per 10 000 000 framesmillions0.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.

ResponseEffect on control traffic
widen the digest to 64 bitsnone
deepen the tableworse — more live frames, same key space
vary a core octet in the generatorthe fix, and it is a stimulus change
report lookup_ambiguous and exclude those framesthe 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

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ---------------------------------------------------------------------
// 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
endmodule

Classification: 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

One parameter, the number of octets a beat holds, appears as eleven different literal constants across four chapters. The coverage chapter writes it three times: sixty four residues, sixty four beat start offsets, and a dual frame dimension of two values. The same chapter's exclusion arithmetic writes it twice more, as one residue in bucket zero and sixty three in bucket one. Its loopback reachability writes sixteen. The generation chapter writes it four times, as a residue target of sixty four, a phase target of sixty four, a spill size target of twenty three, and a greatest common divisor taken against sixty four. None of the eleven names the width. Five of the eleven move when the width moves and three of those five do not move proportionally: the dual frame dimension disappears entirely below twenty one octets, the reachable offset count is the width over four, and the spill size target moves inversely, from one hundred and eighty two members at eight octets to twenty three at sixty four and eleven at one hundred and twenty eight. The remaining constants, the runt threshold of forty three and the reorder target of four, are counts of octets and of responses and do not move at all.BEAT_OCTETSone parameterResidue dimension20.4 — WOffset dimension20.4 — WDual-framedimension2 only if W is over 20Reachable offsetsW over 4Spill-size target182, 23, 11 — inverseRunts: 43octets, not beatsThe cross: 210 Wsquared128x from 8 to 64One block owns itSection 5's adapter12
Figure 2 — eleven constants in four files, and five of them move when one parameter does.

Module 20 contains eleven constants that look like independent design choices and are the same number.

WhereWritten asActually
Chapter 20.4 §2's residue dimension64 valuesW
Chapter 20.4 §2's offset dimension64 valuesW
Chapter 20.4 §2's dual-frame dimension2 values2 if W > 20, else 1
Chapter 20.4 §4's bucket 0 residues1 of 641 of W
Chapter 20.4 §4's bucket 1 residues63 of 64min(63, W)
Chapter 20.4 §4's loopback offsets16max(1, W/4)
Chapter 20.1 §9's reachable phases16 of 64max(1, W/4) of W
Chapter 20.1 §15's residues_count target64W
Chapter 20.1 §15's phases_count target64W
Chapter 20.1 §15's c_spill_sizes target23the count of sizes ≡ 3 (mod W)
Chapter 20.1 §17's phase count64 / 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.

WidthBeat, octetsGap plus preambleDual-frame beat possible?
1 Gb/s GMII117 to 20no
10 and 25 Gb/s817 to 20no
100 Gb/s CGMII6417 to 20yes
a 1 024-bit projection12817 to 20yes

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.

WidthDeclared offsetsReachableShare
111100%
88225%
64641625%
1281283225%

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:

WidthSpill sizes in rangeChapter 20.1 §15's target is "more than 23"
10 — the case does not existUNREACHABLE
8182met by 12.5% of a uniform sweep
6423UNREACHABLE by one — the target was written as a strict inequality
12811UNREACHABLE — 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 boundaryChapter 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

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ---------------------------------------------------------------------
// 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
endmodule

Classification: 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 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.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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 × D

At 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.

WidthDeclaredLegalLegal shareLoopback reachableOf legal
1 — GMII1056965.7%69100.0%
8 — 10 and 25 Gb/s6 7203 74455.7%93625.0%
64 — CGMII860 160467 32854.3%116 83225.0%
128 — a projection3 440 6401 721 08850.0%430 27225.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 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.

CGMIIGMII
live dimensions63
declared cells860 160105
ratio8 192×
what closure meansa week of soaka 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

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ---------------------------------------------------------------------
// 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);
endmodule

Classification: 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 zeroChapter 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.

BlockWidth-dependent?What moves
Section 3's packageone enum valueoctets_of, and nothing else
Section 3's uid allocatornoa uid is a counter
Section 5's beat-width adapterentirelyit is the width
Section 7's monitorpartlythe offset and residue it reports
Section 9's sequence layernoone input value — Section 9
Section 11's driverentirelybeat assembly is the width
Section 13's observation joinnoit joins on a uid
Section 15's telemetryits thresholdsSection 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 numberLives inMoves with W?
the declared crossChapter 20.4 §3yes — as
the legal crossChapter 20.4 §4yes, and not proportionally
the reachable offsetsChapter 20.1 §9yes — W/4
the spill-size targetChapter 20.1 §15yes, and inverts
the phase-degeneracy ruleChapter 20.1 §17yes — gcd with W
the runt threshold, 43Chapter 19.7 §7no
the reorder target, 4Chapter 19.6 §21no
the invariant core shareChapter 20.3 §2no

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.

SurvivesValueWhy
the loopback's share of legal25.0%W/4 of W — Section 6
the 1 516 to 1 519 degeneracy1 phase, every widthperiod 1 536 divides by 8, 64 and 128
the runt useful range43 lengthsChapter 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

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ---------------------------------------------------------------------
// 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;
endmodule

Classification: 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.

BitTarget at W = 64General formMoves?
l_residuesresidues_count == 64== Wyes — linear
l_spillc_spill_sizes > 23one less than the count of sizes ≡ 3 mod Wyes — inverse
l_runtsc_runts > 43unchangedno
l_phasesphases_count == 64== W, reachable W/4yes — linear
l_reorderpeak_held >= 4unchangedno

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.

WidthSpill sizes that existTarget > 23Reachable?
10> 23no — the case does not exist
8182> 23yes, easily
6423> 23no — by exactly one
12811> 23no

The same constant is easy at 10 Gb/s, impossible by one at 100 Gb/s and impossible by twelve at 128 octetsand 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:

WidthReachable phasesSizes reaching all of themSizes reaching exactly one
82728 — 50.0%727 — 50.0%
6416728 — 50.0%91 — 6.3%
12832728 — 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 = 8At W = 64
a size reaching one phase50% of the offsets6.25% of the offsets
how it reports"half covered""a sixteenth covered"
what changednothing about the testnothing about the test

And the mean across all sizes is 75.0% of reachable phases at W = 8 against 66.8% at W = 64so 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

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ---------------------------------------------------------------------
// 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
endmodule

Classification: 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.

NeverBecause
1let two live frames share a join slotSection 13's direct map silently evicts
2share a uid space between two agentsthe join attributes one link's frames to another's
3let the monitor guess on an ambiguous digestSection 7 — a wrong row is worse than no row
4modify the frame to carry the identifierSection 2 — it changes which paths the frame takes
5hardcode a beat count anywhere outside Section 5Section 6's eleven constants
6report a percentage without its orphan countsSection 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 fivea hardcoded 64 is correct at one width and wrong at three
row sixan 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

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ---------------------------------------------------------------------
// 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
endmodule

Classification: 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 denominatorsand 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

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ---------------------------------------------------------------------
// 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
endmodule

Classification: 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

A loopback regression at one hundred gigabits per second closes all one hundred and sixteen thousand eight hundred and thirty two cells it can reach and the tool reports one hundred per cent. Split those cells by their error class coordinate. Thirty six thousand eight hundred and sixty four of them, thirty one point five five per cent, carry the error class none, and only those frames reach the scoreboard's receive side to be compared octet for octet. The remaining seventy nine thousand nine hundred and sixty eight cells, sixty eight point four five per cent, carry one of the four error classes, and the frame validity rules require the design to discard every one of those frames, so the scoreboard's check is the correct but one bit expectation that the frame did not arrive. Now split the thirty one point five five per cent again, by how much of each surviving frame the comparison covers. On a sixty four octet control frame the invariant core is fifteen octets, twenty three point four per cent, so the composite is seven point three eight per cent. On a sixty four octet frame carrying forty six payload octets the core is ninety three point eight per cent and the composite is twenty nine point six. On a fifteen hundred and eighteen octet frame the core is ninety nine point seven per cent and the composite is thirty one point five. The difference between the first and the second is twenty two point two points and costs forty five octets of payload; the difference between the second and the third is one point nine points.The tool says100%116 832 of 116 832Error classes79 968 — 68.45%Error class none36 864 — 31.55%It did not arrivecorrect, and one bitThe invariantcore20.3 Section 2Control: 23.4%composite 7.38%46 payload: 93.8%composite 29.60%1 518: 99.7%composite 31.46%45 octets buys22.2the cheap change12
Figure 3 — a hundred per cent report, split twice, arrives at seven point four.

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 §4860 160
legal467 328
reachable in a loopback116 832
covered116 832
the tool's report100%

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 classCellsShareScoreboard's check
none36 86431.55%an octet-for-octet comparison
CRC errora negative expectation
alignment errora negative expectation
undersizea negative expectation
oversizea negative expectation
all four error classes79 96868.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.

TrafficCore shareCells × core
64 octets, one payload octet23.4%7.38%
64 octets, 46 payload octets93.8%29.60%
1 518 octets99.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 modelChapter 20.4's 116 832 is correct
a criticism of the scoreboardChapter 20.3's negative expectations are correct
a number any one component could reportit 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.

AssumptionFromIf false
1the digest is unique among live framesthe stimulusSection 4 — 63.1% ambiguous on control traffic
2the uid does not wrap within a run24 bits, 112 msrows from two frames merge silently
3only one agent mints into this uid spaceconventionSection 14's row two — wrong rows, no flag
4the frame is not modified to carry the keySection 2the experiment changed
5BEAT_OCTETS is the only width in the environmentSection 5Section 6's eleven constants disagree
6the gap is chosen in octets, not beatsChapter 19.3 §6every phase number moves — Section 11
7the wire period is a multiple of fourChapter 19.3 §6's deficitoffsets_reachable is W, not W/4
8the scoreboard's verdict arrives before retirementChapter 20.3 §11c_first_hit_checked reads zero
9the coverage sampler's first-hit bit is per frameChapter 20.4 §9the 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 assumedWhy not
that a missing observation means a lost frameChapter 20.3 §13's five legal drops
that the four components agreethe disagreement is the measurement — Section 17
that the agent is boundagent_inertChapter 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 runa 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.

BlockFlopsNature
uvmagent_pkg0types
uid_allocator — 256 entries × 57 bits~14 721the identifier table
beat_width_adapter0combinational
agent_monitor~96counters
agent_sequence_layer~64an LFSR and a counter
agent_driver~90beat shaping
observation_join — 1 024 rows × 72 bits~74 017the join table
agent_telemetry0combinational
agent_conformance_monitor0combinational
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 asSizeVerdict
flip-flops88 988absurd — six MACs' worth
memory10.86 KiBtrivial — a simulator allocates it and forgets
against Chapter 19.5 §3's receive FIFO33.9% of 32 KiBa 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.

TableDepthBound
uid_allocator256live digests — Chapter 19.1 §6's 12 in the pipeline, plus the FIFO's
observation_join1 024Chapter 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:

FlopsShare
stimulus — Chapter 20.1 and Chapter 20.5~2 0402.2%
checking — Chapter 20.2 and Chapter 20.3~2 4902.6%
measurement — Chapter 20.4~6500.7%
identity — this chapter~88 98894.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

Once the agent gives both monitors the same frame identifier, the first property anybody writes asserts that the frame's beat start offset is the same on both sides. Within one width it is a tautology: both monitors compute the start octet modulo the same width, so they agree by construction and the property has no content. Check it at one octet per beat, at eight, at sixty four and at one hundred and twenty eight, and it passes at all four. Now bind it across a width converter, where a sixty four octet beat domain meets an eight octet one. The wire side's reachable offsets are the sixteen multiples of four, because the transmit assembler's deficit makes every wire period a multiple of four. The system side sees the same frame at that offset taken modulo eight. The two are equal only when the wire side offset is below eight, which is offsets zero and four: two of sixteen, twelve and a half per cent. So the property is false on eighty seven and a half per cent of the reachable offsets, and it passes on every frame of a fifteen hundred and eighteen octet stress test, because such a stream visits offset zero and only offset zero forever. The repair is to name both widths, asserting that the system side offset equals the wire side offset modulo the system width, or to name neither, asserting instead that the frame's length in octets is unchanged across the converter.offset ispreservedthe tempting propertyAt W = 1, 8, 64,128a tautology — passesAcross aconverterW is 64 and 8 at onceWire side: 16offsetsthe multiples of 4System sidethat offset mod 8Equal only under8offsets 0 and 4True on 12.5%false on 87.5%1 518 sits atoffset 0so it always passesName both, orneitherclass 9312
Figure 4 — correct at every width, and false on seven eighths of the offsets between two.

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.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// 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.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// 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.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// 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.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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.

#ScenarioExpected
1one mintuid 0, minted_valid
21 000 mintsc_minted = 1 000, uids 0 to 999
3a lookup on a minted digestlookup_hit, the right uid
4a lookup on an unminted digestno hit; not an error
5two mints with the same digestc_collisions = 1
6a lookup after thatlookup_ambiguous
716 777 216 mintsuid_wrapped; the run is void
8257 mints, then a lookup on the firstthe entry was overwritten — a miss
9a mint with TABLE_DEPTH = 1every lookup after one mint misses
10reset mid-runall valid bits clear; uids restart at 0

The beat-width adapter — 12 scenarios.

#ScenarioExpected
11W = 64, wire_len = 641 beat; residue 60
12W = 64, wire_len = 1 47524 beats; residue 63 — the spill size
13W = 8, wire_len = 648 beats; residue 4
14W = 1, wire_len = 6464 beats; residue 0 always
15W = 64, start octet 37offset 37 — legal in the type, unreachable in the design
16the same, driven by Section 11offset is always a multiple of 4
17W = 64, wire_len = 1 518phases_this_size = 1, size_is_degenerate
18W = 64, wire_len = 1 520phases_this_size = 16
19W = 8, wire_len = 1 518phases_this_size = 1 of 2 reachable
20W = 128, wire_len = 1 518still 1 — period 1 536 divides 128
21W = 8, any sizespans_two_beats never asserts
22W = 64, minimum sizespans_two_beats asserts

The sequence layer — 9 scenarios.

#ScenarioExpected
23w_spill = 0lengths uniform; spill sizes at chance
24w_spill = 50, spill_modulus = 64half the frames at L ≡ 3 (mod 64)
25the same with spill_modulus = 8half at L ≡ 3 (mod 8) — a different set
26spill_modulus = 0spill_target_impossible; no adjustment
27W = 128 with the 23 targetspill_target_impossible at elaboration
28allow_illegal clearno frame below 64 — Chapter 19.7 §7 unreachable
29allow_illegal set, w_runt = 5runts in 5 to 47 only
30grep the block for "beat"no match — the reuse claim, checked
31the same items at three widthsbyte-identical item streams

The driver — 8 scenarios.

#ScenarioExpected
32W = 64, L = 641 data beat, then an idle beat
33W = 64, L = 652 data beats; valid_octets = 1 on the last
34W = 8, L = 659 beats; valid_octets = 1
35the gap counter held in beatsperiod becomes L + 72 — every phase moves
36the gap counter held in octetsperiod L + 20 − (L mod 4)
371 000 frames at L = 1 518every frame starts at offset 0
381 000 frames at L = 1 520all 16 reachable offsets
39a passive buildpull never asserts

The join — 11 scenarios.

#ScenarioExpected
40plan, observe, retire, in orderone closed row
41observe with no planc_orphan_obs
42plan, retire, no observec_orphan_plan
431 025 plans without retirementtable_overflow
44two uids 1 024 apartthe same slot — an eviction
45a retire on a stale uidignored; no counter moves
46a first-hit frame that is comparedc_first_hit_checked
47a first-hit frame that is discardedc_first_hit_bare
48a scoreboard verdict 631 cycles latejoined — the row is still live
49a verdict 100 000 cycles latethe row was evicted at 1 024
50all rows retiredjoin_coverage_pct = 100

The joined numbers — 8 scenarios.

#ScenarioExpected
51a loopback run, control trafficoctet_weighted_pct ≈ 7
52the same with 46-octet payloads≈ 30
53the same at 1 518 octets≈ 31
54a run with the injector armedc_first_hit_bare climbs
55a 40% orphan ratejoin_thin; the numbers are void
56uid_wrappedjoin_untrustworthy, identifier_corrupt
57a 10 Gb/s build's telemetry at 100 Gb/sphase_space_stale
58merging a W = 8 database into W = 64agent_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.

RunWWhat it establishes
A1 — GMII3 of 6 coverage dimensions are degenerate
B8 — 10 Gb/sp_offset_preserved passes
C64 — CGMIIp_offset_preserved passes
D128 — projectionp_offset_preserved passes
E64 and 8, across a converterp_offset_preserved FAILS on 87.5% of offsets

The oracle is four-part, and each part is checkable independently.

PartRun ARun BRun CRun DRun E
declared cross1056 720860 1603 440 640both, and incomparable
reachable offsets12163216 and 2
p_offset_preservedpassespassespassespassesfails, 87.5%
the item streamidenticalidenticalidenticalidenticalidentical

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."

CheckIf yesMeaning
c_observed moving?nothe monitor is not bound — Chapter 20.2 §10
c_unattributed = c_observed?yesa passive agent, working correctly
ambiguous_pct_x10 high?yesSection 4 — the stimulus, not the digest
table_overflow?yesrows 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."

CheckIf yesMeaning
is the traffic control frames?yesSection 17 — 23.4% core, 31.55% non-error cells
join_coverage_pct above 90?yesthe 7 is real
c_first_hit_bare large?yescells closed by discarded frames
is the injector armed?yes68% 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."

CheckIf yesMeaning
beat_octets = 8?yesthe build is what you think
dual_dimension_live = 0?yesa whole dimension is gone — Section 6
declared cells = 6 720?yes128× smaller, correctly
did the percentage rise?usuallya 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."

CheckIf yesMeaning
spill_target_impossible?yesthe answer, at elaboration
what is beat_octets?64the set has exactly 23 members
is the target > 23?yesa strict inequality over a 23-member set
at W = 128?11 membersimpossible 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."

CheckIf yesMeaning
are the two monitors at different widths?yesSection 20's rejected property
is obs_sys.offset = obs_wire.offset mod 8?yesboth are right; the property is wrong
is obs_wire.offset under 8?nowhich is why it fails on 87.5% of offsets
does it pass at 1 518?yesoffset 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."

CheckIf yesMeaning
different beat_octets?yestwo different cross spaces
was agent_sound checked?noSection 16 exists for this
are the bin names the same?yeswhich is why the tool merged them
is the union meaningful?noa 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:

SymptomBlamed onUsually is
a low checked-closure percentagethe scoreboardthe error-class share — Section 17
a coverage number that moved 128×a regressionthe beat width — Section 8
a property that passes and should notthe designa 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 octetsChapter 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 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.

ChapterBuildsIts number
Chapter 20.1the generatorfive lists, and which weights cannot reach them
Chapter 20.2the assertion librarythe fired percentage over a measured corpus
Chapter 20.3the scoreboards23.4% of a control frame is comparable
Chapter 20.4the coverage model860 160, 467 328, 116 832 — three denominators
Chapter 20.5the error injector14 design paths, and the 3 no frame reaches
this chapterthe agent7.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 logicChapter 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 somewhereand 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

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.