Ethernet · Module 21
A Method for Debugging Ethernet
The receive path has twelve fault sites and the RMON-required counters separate them into seven classes, one holding five — so the method's ceiling is knowable from a datasheet.
A debugging method is a decision procedure over a space of candidate faults, and the first thing to know about it is how many of those candidates the instrumentation can actually tell apart.
| Count | |
|---|---|
| fault sites on the receive path | 12 |
| counters a MAC could expose | 11 |
| counters the RMON standard requires | 6 |
| classes the RMON six separate the 12 sites into | 7 |
| sites in the largest of those classes | 5 |
| observations a perfect procedure needs | 7 |
| information-theoretic floor | 4 |
Rows four and five are the chapter's subject. Five of the twelve sites — the address filter, VLAN membership, the receive FIFO, the DMA path and the host driver — produce identical readings on every counter the standard requires. A frame arrives, a frame does not reach the host, and no error counter moves, because none of those five is an error.
Rows six and seven are the chapter's second subject. Twelve candidates is 3.58 bits, so four binary observations should suffice. Seven are needed, because the counters do not partition the space evenly — four of them split off exactly one site each, and a question that eliminates one candidate in twelve carries 0.414 bits.
And the counter everybody looks at first carries none at all.
1. Scope, and a Fault Space With Twelve Members
Scope: the receive path, from the channel to host memory, as a space of fault sites; the counters that observe it; and what a procedure over the two can and cannot conclude.
Not in scope: the individual failure modes. Chapter 21.2 is the catalogue and Chapter 21.3 is the first entry in it. This chapter is the method, and a method is worth writing down separately because its quality is measurable — Sections 4, 6 and 8 measure it.
Start by enumerating the space, because everything after depends on it being the right size.
| # | Site | Named in |
|---|---|---|
| 1 | the channel — cable, connector, optics | Chapter 3.1 |
| 2 | the PHY's lane alignment | Chapter 3.4 |
| 3 | the xMII interface itself | Chapter 4.2 |
| 4 | preamble and SFD detection | Chapter 5.2 |
| 5 | the frame parser's offset arithmetic | Chapter 19.2 §3 |
| 6 | the CRC engine's own logic | Chapter 19.4 |
| 7 | the validity classifier | Chapter 7.3 §3 |
| 8 | the address filter | Chapter 7.4 |
| 9 | VLAN port membership | Chapter 13.2 |
| 10 | the receive FIFO | Chapter 19.5 |
| 11 | the DMA and descriptor path | Chapter 19.6 |
| 12 | the host driver and its buffers | Chapter 18.3 |
Twelve, and the number matters more than the list does. A procedure that starts from "the link is broken" is searching a space of twelve, which is 3.585 bits — so four well-chosen binary observations would be enough if the observations were well chosen, and Section 8 shows they cannot be.
Three things this list deliberately is and is not.
| Choice | |
|---|---|
| it is receive-only | the transmit path is a different twelve |
| it is sites, not symptoms | a symptom is what you see; a site is what you replace |
| it stops at the driver | above that is somebody else's decision procedure |
Row two is the distinction the whole chapter turns on. "CRC errors are climbing" is a symptom and it is consistent with sites 1, 2, 3, 5 and 6 — five of the twelve. Chapter 21.2 is the catalogue of symptoms; this chapter is about getting from a symptom to a site, and the distance between those two is exactly what Section 4 measures.
2. The Observable Set, and What It Actually Separates
Eleven counters. Six are required by the RMON standard and five are a vendor's choice, and the split is not where anybody would put it.
| Counter | Required? | Sites it moves for |
|---|---|---|
c_crc_errors | yes — Chapter 19.7 §2 | 5 of 12 |
c_alignment_errors | yes | 3 |
c_undersize | yes | 3 |
c_oversize | yes | 2 |
frames_in | yes | 4 |
frames_out | yes | 12 |
c_symbol_errors | no — in the PHY, over MDIO | 2 |
c_filtered | no | 2 |
c_vlan_discards | no | 2 |
c_fifo_drops | no | 1 |
c_desc_errors | no — in the driver | 1 |
Every one of the five optional counters observes a site that produces no error. A filtered frame is not an error, a non-member VLAN frame is not an error, a FIFO drop under a stalled consumer is not an error, and a descriptor the driver did not post is not an error either — Chapter 20.3 §13 listed exactly this family. The standard requires counters for the error cases and leaves the non-error cases to the vendor, which is defensible and is the reason the largest indistinguishable class exists.
Now build the table that decides everything: which counters move for which sites.
| Site | crc | align | under | over | in | out |
|---|---|---|---|---|---|---|
| 1 channel | yes | yes | yes | no | yes | short |
| 2 PHY lanes | yes | yes | no | no | yes | short |
| 3 xMII | yes | yes | no | no | yes | short |
| 4 preamble | no | no | no | no | yes | short |
| 5 parser offset | yes | no | yes | yes | no | short |
| 6 CRC logic | yes | no | no | no | no | short |
| 7 validity | no | no | yes | yes | no | short |
| 8 filter | no | no | no | no | no | short |
| 9 VLAN | no | no | no | no | no | short |
| 10 FIFO | no | no | no | no | no | short |
| 11 DMA | no | no | no | no | no | short |
| 12 driver | no | no | no | no | no | short |
Rows eight to twelve are identical. Five sites, one signature, and the signature is "frames went in, fewer came out, nothing else moved."
Rows two and three are identical as well, which is a smaller problem and a real one: a PHY lane-alignment fault and an xMII timing fault both produce CRC errors and alignment errors on frames that were counted at the port. The difference between them is a symbol-error counter inside the PHY, which is row seven of the previous table and is optional.
| Classes | Largest class | |
|---|---|---|
| with the RMON six | 7 | 5 |
| with all eleven | 12 | 1 |
Five optional counters are the difference between a diagnosis and a shortlist of five.
And it is worth walking the twelve rows once, because each one is a chapter's failure mode reduced to a counter signature, and three of them are counter-intuitive.
| Site | The fault | Why the signature is what it is |
|---|---|---|
| 1 channel | a marginal connector, a bent fibre | the frame is damaged in transit, so the port counts it and the check fails; short frames appear because a burst can truncate one |
| 2 PHY lanes | Chapter 3.4's deskew is wrong | octets arrive in the wrong order, so the check fails and the frame is not whole octets — but the frame was seen, so frames_in moves |
| 3 xMII | a setup violation on the interface | identical to row two from outside the chip; only a PHY-internal symbol counter tells them apart |
| 4 preamble | Chapter 5.2's SFD is missed | the frame is never framed, so nothing downstream sees it — frames_in moves and nothing else can |
| 5 parser offset | Chapter 19.2 §3's barrel is off | the length field is read from the wrong octets, so undersize and oversize both move — and frames_in does NOT, because the port counted the frame correctly |
| 6 CRC logic | Chapter 19.4's residue is wrong | good frames are declared bad; only c_crc_errors moves, and the frames were perfect |
| 7 validity | Chapter 7.3 §3's bounds are wrong | undersize and oversize move on conformant frames; nothing else does |
| 8 filter | Chapter 7.4's table is wrong | a legal discard — no error, so no required counter moves |
| 9 VLAN | port membership is wrong | a legal discard, same signature |
| 10 FIFO | Chapter 19.5 §14's overflow | a legal drop under a stalled consumer, same signature |
| 11 DMA | Chapter 19.6's descriptors | no descriptor, no delivery, no error |
| 12 driver | buffers not reaped | same, one level up |
Rows five and six are the two that surprise people and they surprise in opposite directions.
Row five's frames_in is the one entry in the table that reads "no" where instinct says yes. The port's frame counter increments when a frame is received — at the Chapter 5.2 delimiter, before anything is parsed — so a parser that misreads the length has not affected it at all. That single "no" is what separates site 5 from sites 1, 2 and 3, and it is the second-best question in the method precisely because it is counter-intuitive.
Row six is the case where the frames were never damaged. A fault in Chapter 19.4's residue comparison declares good frames bad, so c_crc_errors climbs on a perfect wire — and every instinct points outward at a cable. The distinguishing evidence is that nothing else moves: no alignment errors, no size errors, and frames_in unaffected. A CRC counter climbing alone is the signature of the check engine, not of the channel.
3. RTL 1 — The Debug Package and the Candidate Mask
// ---------------------------------------------------------------------
// debugm_pkg -- a fault space as a bit mask, and the observations that
// narrow it.
//
// The whole method is: start with twelve bits set, and clear bits.
// Writing it that way makes two things checkable that a prose
// procedure hides -- whether an observation narrowed anything, and
// whether the remaining set is a single site or a class.
// ---------------------------------------------------------------------
package debugm_pkg;
// Section 1's twelve sites, in receive order. The ORDER matters:
// Section 7's conservation checker cuts the space at a boundary,
// and a boundary is only meaningful if the sites are ordered.
typedef enum logic [3:0] {
SITE_CHANNEL = 4'd0,
SITE_PHY_LANES = 4'd1,
SITE_XMII = 4'd2,
SITE_PREAMBLE = 4'd3,
SITE_PARSER = 4'd4,
SITE_CRC_LOGIC = 4'd5,
SITE_VALIDITY = 4'd6,
SITE_FILTER = 4'd7,
SITE_VLAN = 4'd8,
SITE_FIFO = 4'd9,
SITE_DMA = 4'd10,
SITE_DRIVER = 4'd11
} site_e;
typedef logic [11:0] site_mask_t;
localparam site_mask_t ALL_SITES = 12'hFFF;
// Section 2's table, one row per counter: the sites at which a fault
// makes this counter move. These are the ONLY facts the method has.
localparam site_mask_t M_CRC = 12'b0000_0011_0111; // 0,1,2,4,5
localparam site_mask_t M_ALIGN = 12'b0000_0000_0111; // 0,1,2
localparam site_mask_t M_UNDERSIZE = 12'b0000_0101_0001; // 0,4,6
localparam site_mask_t M_OVERSIZE = 12'b0000_0101_0000; // 4,6
localparam site_mask_t M_FRAMES_IN = 12'b0000_0000_1111; // 0,1,2,3
localparam site_mask_t M_SYMBOL = 12'b0000_0000_0011; // 0,1
localparam site_mask_t M_FILTERED = 12'b0000_1000_0000; // 7
localparam site_mask_t M_VLAN_DISC = 12'b0001_0000_0000; // 8
localparam site_mask_t M_FIFO_DROP = 12'b0010_0000_0000; // 9
localparam site_mask_t M_DESC_ERR = 12'b0100_0000_0000; // 10
// Section 4: the RMON-required six leave these five sites with one
// signature between them. Naming the class is what stops a report
// from claiming a site.
localparam site_mask_t CLASS_SILENT = 12'b1111_1000_0000; // 7..11
typedef enum logic [1:0] {
EV_MOVED = 2'd0, // the counter is climbing
EV_ZERO = 2'd1, // it is not -- Section 10 on what that means
EV_ABSENT = 2'd2 // the counter is not implemented
} evidence_e;
function automatic int popcount12(site_mask_t m);
int n = 0;
for (int i = 0; i < 12; i++) if (m[i]) n++;
return n;
endfunction
endpackageClassification: a package that is one table, and the table is the chapter's entire factual content.
What it teaches: that a debugging method can be written as a set intersection, and that writing it that way makes the two useful questions mechanical. "Did that observation narrow anything?" is a mask comparison; "have we reached a site or a class?" is a population count. A prose procedure answers neither, which is why a prose procedure can run for a day without anybody noticing it stopped making progress.
And it teaches that EV_ABSENT is a third value and not a missing one. A counter that is not implemented is different from a counter reading zero: zero is evidence, absence is not. Section 2 found five optional counters, so EV_ABSENT is the common case on the observations that matter most, and a method that treats absence as zero concludes the fault is not at a site it cannot see.
Deliberately simplified: the masks are literals rather than derived from a machine-readable model of the design, so a design change silently invalidates them. CLASS_SILENT is named once here and used as a constant, where it should fall out of the RMON subset by construction — Section 16's monitor recomputes it and compares. And the whole table is receive-only, so a transmit fault presents as twelve candidates none of which is right.
Production implication: the ordering of site_e is load-bearing and it is the cheapest thing in the block. Chapter 20.3 §13's conservation identity cuts the path at the MAC's output, and a cut is only a cut if the sites on each side are contiguous in the enumeration. Sites 0 to 6 are before the MAC's frame counters and sites 7 to 11 are after — one comparison separates them, and it is the first observation Section 12's procedure makes.
// ---------------------------------------------------------------------
// candidate_mask -- the state of a diagnosis: which sites are still
// possible, and whether the last observation helped.
//
// narrowed_nothing is the output that matters. A procedure whose
// observations stop narrowing has finished, and "finished" is not the
// same as "solved" -- Section 4.
// ---------------------------------------------------------------------
module candidate_mask
import debugm_pkg::*;
(
input logic clk,
input logic rst_n,
input logic restart,
input logic obs_valid,
input site_mask_t obs_consistent, // sites consistent with the reading
input evidence_e obs_kind,
output site_mask_t candidates,
output logic [3:0] n_candidates,
output logic narrowed_nothing,
output logic converged, // exactly one site left
output logic class_only, // several, and none separable
output logic [3:0] n_observations
);
site_mask_t next_mask;
always_comb next_mask = candidates & obs_consistent;
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n || restart) begin
candidates <= ALL_SITES;
n_observations <= 4'd0;
narrowed_nothing <= 1'b0;
end else if (obs_valid && obs_kind != EV_ABSENT) begin
narrowed_nothing <= (next_mask == candidates);
candidates <= next_mask;
n_observations <= n_observations + 4'd1;
end
end
assign n_candidates = 4'(popcount12(candidates));
assign converged = (n_candidates == 4'd1);
// Section 4: the five silent sites survive every RMON observation
// together, so a mask equal to CLASS_SILENT is a finished diagnosis
// that has not identified a site.
assign class_only = (candidates == CLASS_SILENT);
endmoduleClassification: a state machine with one state variable, whose interesting outputs are both about having stopped.
What it teaches: that converged and class_only are different terminal conditions and a method must distinguish them. A procedure that runs until n_candidates stops falling reaches CLASS_SILENT on every one of the five silent faults — and reports "five candidates" rather than a site. That is the correct answer and Section 20's rejected property is what happens when somebody asserts it cannot occur.
And it teaches why obs_kind != EV_ABSENT gates the update. An absent counter contributes no mask — not the empty mask, which would clear every candidate, and not the full mask, which would be free. It contributes nothing and must not increment n_observations either, because a procedure that counts unavailable observations as steps reports progress it did not make.
Deliberately simplified: obs_consistent is supplied by the caller rather than derived from a counter reading and its mask, so the block cannot check that the caller used the right table. narrowed_nothing is a single-cycle flag and not sticky. And class_only compares against exactly one class, where Section 4 found two — the silent five and the PHY/xMII pair.
Production implication: n_observations against n_candidates is the number that tells a team whether its instrumentation or its method is the problem. Section 8 derives the optimum: 12 candidates should fall to 1 in seven observations. A diagnosis that has made ten observations and has five candidates left has an instrumentation problem, not a method problem — and the two conclusions lead to entirely different meetings.
4. Seven Classes, and the One That Holds Five
Run Section 2's table through Section 3's intersection and the twelve sites collapse. With the RMON-required six counters, the distinct signatures are these.
| Class | Sites | Signature |
|---|---|---|
| A | channel | crc, align, undersize, frames_in all move |
| B | PHY lanes, xMII | crc, align, frames_in — no undersize |
| C | preamble/SFD | frames_in moves, nothing else |
| D | parser offset | crc, undersize, oversize — frames_in does NOT |
| E | CRC logic | crc only |
| F | validity classifier | undersize and oversize only |
| G | filter, VLAN, FIFO, DMA, driver | nothing moves |
Seven classes from twelve sites, and the useful way to read that is as the method's ceiling: no procedure using only the required counters can do better than name one of these seven.
Class G is the problem and it is worth stating exactly what it contains.
| Site in class G | What happened to the frame | Is it an error? |
|---|---|---|
| the address filter | not addressed to this port | no — Chapter 7.4 |
| VLAN membership | the port is not in that VLAN | no — Chapter 13.2 |
| the receive FIFO | the consumer stalled | no — Chapter 19.5 §14 |
| the DMA path | no descriptor was posted | no — Chapter 19.6 |
| the host driver | the buffer was not reaped | no |
Every member of class G is a legal discard, which is why no error counter moves — and three of the five are Chapter 20.3 §13's legal drop reasons, the other two being past the MAC entirely. The instrumentation is behaving correctly and the fault is invisible for exactly that reason.
Class B is the smaller version of the same thing. A PHY lane-alignment fault and an xMII timing fault produce the same six readings, and the counter that separates them — c_symbol_errors — lives inside the PHY and is reached over MDIO, which is Chapter 4.5's interface and is not always wired.
Now add the optional counters one at a time, best first, and watch the classes.
| Added | Classes | Largest class |
|---|---|---|
| RMON six only | 7 | 5 |
+ c_symbol_errors | 8 | 5 |
+ c_filtered | 9 | 4 |
+ c_vlan_discards | 10 | 3 |
+ c_fifo_drops | 11 | 2 |
+ c_desc_errors | 12 | 1 |
Row two is the shape of the whole problem. Adding the symbol-error counter splits class B and does nothing at all to class G — it raises the class count and leaves the largest class exactly where it was. The four counters that matter are the four that observe non-errors, and each of them splits off exactly one site.
Class G does not shrink by cleverness. It shrinks one site per counter, and the counters are the ones no standard requires.
Which gives the method its most useful precondition, and it is a question to ask before any waveform is opened:
| Question | If no |
|---|---|
is c_filtered implemented? | the filter stays in the shortlist forever |
is c_vlan_discards implemented? | VLAN membership stays |
is c_fifo_drops implemented? | the FIFO stays — and it is Chapter 19.5 §14's truncation path |
is c_desc_errors readable? | the DMA and the driver are one candidate |
Four yes/no questions, asked of a datasheet rather than of a waveform, decide whether the coming investigation has five candidates or one. They take a minute and they are almost never the first thing anybody does.
5. RTL 2 — The Symptom Classifier
// ---------------------------------------------------------------------
// symptom_classifier -- turn a set of counter readings into the mask
// of sites consistent with them.
//
// The block is trivial and the discipline is not: a reading that is
// ZERO narrows the space by INTERSECTING WITH THE COMPLEMENT, and a
// reading that is ABSENT narrows it by nothing at all. Conflating the
// second with the first is the most common error in this chapter and
// Section 10 measures what it costs.
// ---------------------------------------------------------------------
module symptom_classifier
import debugm_pkg::*;
(
input logic clk,
input logic rst_n,
input logic sample,
input logic [31:0] d_crc, // deltas over the sample window
input logic [31:0] d_align,
input logic [31:0] d_undersize,
input logic [31:0] d_oversize,
input logic [31:0] d_frames_in,
input logic [31:0] d_frames_out,
// Which of the optional counters this platform actually has.
input logic [4:0] optional_present, // symbol, filtered, vlan, fifo, desc
input logic [31:0] d_symbol,
input logic [31:0] d_filtered,
input logic [31:0] d_vlan_disc,
input logic [31:0] d_fifo_drop,
input logic [31:0] d_desc_err,
output site_mask_t consistent,
output logic consistent_valid,
output logic [3:0] observations_usable,
output logic symptom_is_silent // Section 4's class G
);
site_mask_t m;
logic [3:0] usable;
// One helper, applied ten times. A counter that moved keeps the
// sites it moves for; a counter at zero keeps the complement.
function automatic site_mask_t apply(site_mask_t cur, logic present,
logic [31:0] delta, site_mask_t moves);
if (!present) return cur; // EV_ABSENT -- no evidence
else if (delta != 32'd0) return cur & moves;
else return cur & ~moves;
endfunction
always_comb begin
m = ALL_SITES;
usable = 4'd0;
m = apply(m, 1'b1, d_crc, M_CRC); usable++;
m = apply(m, 1'b1, d_align, M_ALIGN); usable++;
m = apply(m, 1'b1, d_undersize, M_UNDERSIZE); usable++;
m = apply(m, 1'b1, d_oversize, M_OVERSIZE); usable++;
m = apply(m, 1'b1, d_frames_in, M_FRAMES_IN); usable++;
// d_frames_out is deliberately NOT applied. Section 6.
m = apply(m, optional_present[0], d_symbol, M_SYMBOL);
m = apply(m, optional_present[1], d_filtered, M_FILTERED);
m = apply(m, optional_present[2], d_vlan_disc, M_VLAN_DISC);
m = apply(m, optional_present[3], d_fifo_drop, M_FIFO_DROP);
m = apply(m, optional_present[4], d_desc_err, M_DESC_ERR);
for (int i = 0; i < 5; i++) if (optional_present[i]) usable++;
consistent = m;
observations_usable = usable;
symptom_is_silent = (d_frames_in != 32'd0) &&
(d_frames_out < d_frames_in) &&
(d_crc == 32'd0) && (d_align == 32'd0) &&
(d_undersize == 32'd0) && (d_oversize == 32'd0);
end
assign consistent_valid = sample;
endmoduleClassification: a ten-way intersection whose whole content is one three-valued helper function.
What it teaches: that the three cases of apply are the method. A counter that moved intersects with its mask; a counter at zero intersects with its complement — which is a real narrowing and is the half people forget — and a counter that is absent intersects with nothing. Writing them as one function with a present argument makes the third case impossible to skip, where a procedure written as prose skips it every time.
And it teaches that symptom_is_silent is a named symptom rather than a derived one. It is the signature of Section 4's class G — frames in, fewer out, no error counter moving — and it is worth its own output because it is the single most common real-world Ethernet complaint and the one where the RMON set has already said everything it can.
Deliberately simplified: the six required counters are assumed present, which is what "required" means and is not always true of a switch's per-port statistics. The deltas are supplied rather than sampled, so the block has no opinion about the window length — Section 18's row three. usable++ in a for loop is not synthesisable as written. And a counter that moved by one in a million frames is treated identically to one that moved on every frame, which Section 17 argues is sometimes the whole answer.
Production implication: observations_usable is the number to print at the top of a diagnostic report. It is 5 on a platform with only the required counters — because frames_out is excluded — and 10 on a fully instrumented one. The rest of the report should be read against it: a five-observation diagnosis that names a site has either been lucky or has been wrong, and Section 8 says which is more likely.
6. Why frames_out Carries Zero Bits
Every diagnosis starts by looking at whether frames are reaching the host. That observation carries no information at all, and the arithmetic is one line.
An observation's value is how much it narrows the candidate set. Formally, for a space of twelve sites and a counter that moves for k of them, the reading's entropy is:
H = −p·log2(p) − (1−p)·log2(1−p), p = k / 12| Counter | Moves for | p | Bits |
|---|---|---|---|
c_crc_errors | 5 | 0.417 | 0.980 |
frames_in | 4 | 0.333 | 0.918 |
c_alignment_errors | 3 | 0.250 | 0.811 |
c_undersize | 3 | 0.250 | 0.811 |
c_oversize | 2 | 0.167 | 0.650 |
c_symbol_errors | 2 | 0.167 | 0.650 |
c_filtered | 2 | 0.167 | 0.650 |
c_fifo_drops | 1 | 0.083 | 0.414 |
c_desc_errors | 1 | 0.083 | 0.414 |
frames_out | 12 | 1.000 | 0.000 |
The last row is the finding. frames_out falls short for every one of the twelve sites — that is what "the link is broken" means — so the reading is the same whatever the fault is and it eliminates nothing. It is the observation that starts every investigation and it is worth zero bits.
That is not an argument against looking at it. frames_out is what tells you there is a fault; it is the trigger, not a measurement, and confusing the two is why investigations spend their first hour confirming what the ticket said.
| Role | Bits | |
|---|---|---|
frames_out short | the trigger | 0.000 |
c_crc_errors | the best first question | 0.980 |
frames_in | the second | 0.918 |
c_fifo_drops | a last-resort separator | 0.414 |
Row two is the procedure's actual first step and it is worth knowing why. c_crc_errors moves for the channel, the PHY lanes, the xMII, the parser and the CRC engine's own logic — five of twelve, the closest any counter comes to an even split — so it is the best available question and it is still only 0.98 of a bit. A perfectly balanced question would be worth 1.000.
And the bottom of the table is the reason Section 8's answer is seven and not four. Four of the counters are worth 0.414 bits each, and they cannot be combined: each splits off exactly one site. There is no clever ordering that makes them worth more, because information that is not there cannot be rearranged into existence.
7. RTL 3 — The Conservation Checker
// ---------------------------------------------------------------------
// conservation_checker -- Chapter 20.3 Section 13's identity, used as
// a diagnostic cut rather than as a scoreboard check.
//
// frames_in - frames_out == crc + filtered + dropped + under + over
//
// If the identity HOLDS, every missing frame is accounted for by a
// counter and the fault is at a site the instrumentation names. If it
// does NOT hold, frames are disappearing somewhere no counter watches,
// which is a different and much shorter list.
// ---------------------------------------------------------------------
module conservation_checker
import debugm_pkg::*;
(
input logic clk,
input logic rst_n,
input logic [31:0] d_frames_in,
input logic [31:0] d_frames_out,
input logic [31:0] d_crc,
input logic [31:0] d_undersize,
input logic [31:0] d_oversize,
input logic [31:0] d_filtered, // may be absent
input logic [31:0] d_fifo_drop, // may be absent
input logic [4:0] optional_present,
input logic [31:0] skew_bound, // Chapter 19.1 Section 6's 12
output logic identity_holds,
output logic identity_undecidable,
output site_mask_t cut_mask,
output logic [31:0] unaccounted
);
logic [31:0] accounted, missing;
always_comb begin
missing = d_frames_in - d_frames_out;
accounted = d_crc + d_undersize + d_oversize
+ (optional_present[1] ? d_filtered : 32'd0)
+ (optional_present[3] ? d_fifo_drop : 32'd0);
unaccounted = (missing > accounted) ? (missing - accounted) : 32'd0;
// Chapter 20.3 Section 13: the identity is exact only when the
// pipeline is empty. Under load the counters commit at different
// depths, so a skew of up to twelve frames is not a discrepancy.
identity_holds = (unaccounted <= skew_bound);
// An identity that needs counters this platform does not have
// cannot be evaluated -- and reporting it as FALSE would blame the
// design for the instrumentation.
identity_undecidable = !optional_present[1] || !optional_present[3];
// The cut. If frames are unaccounted for, no counter watches the
// site, so the candidates are the ones with no counter of their
// own: preamble detection, VLAN membership, DMA and the driver.
cut_mask = identity_undecidable ? ALL_SITES
: (identity_holds ? ~12'b1101_0000_1000 : 12'b1101_0000_1000);
end
endmoduleClassification: a conservation law used backwards — as a partition of a fault space rather than as a check on a design.
What it teaches: that Chapter 20.3 §13's identity is worth more as a diagnostic than as an assertion. As an assertion it says "frames are not being lost"; as a cut it says which half of the space to search, because a frame that vanishes without incrementing anything can only have vanished where nothing counts. Four of the twelve sites have no counter of their own — preamble detection, VLAN membership, the DMA path and the driver — and the identity separates those four from the other eight in one arithmetic step.
And it teaches that identity_undecidable must be a distinct output from !identity_holds. The identity needs c_filtered and c_fifo_drops; both are optional. On a platform without them the sum is short by construction, unaccounted is large, and a checker that reports "the identity fails" has reported a missing counter as a design fault.
Deliberately simplified: skew_bound is an input rather than derived, where Chapter 19.1 §6's twelve frames in flight is the correct value at 100 Gb/s and 0.19 at 1 Gb/s — the same constant is wrong by a factor of 63 across the family, which is Chapter 20.6 §6's problem arriving in a debugger. missing can underflow if frames_out exceeds frames_in, which happens across a counter wrap. And the cut mask is a literal, where it should be computed as the sites with no counter mask covering them.
Production implication: the identity is the cheapest step in the whole method and it is usually skipped, because it requires reading six counters and doing arithmetic rather than looking at one. It is worth roughly a bit and a half on its own — four sites against eight — and it needs no waveform, no capture and no traffic generator. A procedure whose first two steps are this identity and c_crc_errors has spent no equipment time and has 3 or 4 candidates left.
8. Seven Observations, Not Four
Twelve candidates is 3.585 bits, so four binary observations is the floor. A perfectly played diagnosis needs seven, and the gap is a property of the counters rather than of the player.
| Observations | |
|---|---|
information-theoretic floor — ceil(log2 12) | 4 |
| optimal decision tree over all eleven counters | 7 |
| optimal tree over the RMON six | 3, to seven classes |
| the ratio | 1.75× |
Row three is not better than row two; it is a different question. With only the required counters the space has seven distinguishable classes, and three observations reach them — ceil(log2 7) is 3, and the RMON counters happen to partition evenly enough to achieve it. The procedure finishes quickly and finishes on a class of five.
Row two is what it costs to go from seven classes to twelve sites, and the cost is four more observations for five more distinctions — a rate of 1.25 observations per site, which is the arithmetic of a linear search and not of a binary one.
Why the tail is linear:
| Stage | Candidates | Best question | Bits |
|---|---|---|---|
| 1 | 12 | c_crc_errors | 0.980 |
| 2 | 5 or 7 | frames_in or c_undersize | ~0.9 |
| 3 | 2 to 5 | c_alignment_errors or c_symbol_errors | ~0.8 |
| 4 | 5 — class G | c_filtered | 0.722 of that subset |
| 5 | 4 | c_vlan_discards | 0.811 |
| 6 | 3 | c_fifo_drops | 0.918 |
| 7 | 2 | c_desc_errors | 1.000 |
Stages four to seven are the linear tail and they are forced. Each of those four counters moves for exactly one site, so each question can only ever answer "is it this one?" — and four such questions are needed to separate five candidates, regardless of order.
A fault space is searchable in
log2(n)steps only when the observations partition it. Ethernet's do not, because four of the five decisive counters are one-site indicators.
Which suggests the one instrumentation change that would matter, and it is not another counter.
| Change | Classes | Observations to a site |
|---|---|---|
| add all four optional counters | 12 | 7 |
| add one counter that moves for 6 of the 12 | 12 | 5 |
| add a per-site "frames entered" counter | 12 | 4 — the floor |
Row three is what a designer can do for a debugger and nobody does. A single counter at each of the twelve boundaries — frames entering the parser, entering the filter, entering the FIFO, entering the DMA — turns the search into a bisection, and a bisection over twelve reaches a site in four observations, which is the floor.
| Flops | |
|---|---|
| Chapter 19.7 §19's counter block — 27 counters and a shadow bank | 2 672 |
| per counter | ~99 |
| twelve boundary counters | ~1 188 |
| Chapter 19.7 §19's MAC datapath | 14 166 |
| the addition, as a share | 8.4% |
Eight and a half per cent of a datapath to take every future localisation from seven observations to four — and the reason nobody spends it is that the benefit accrues to a different team from the one that pays, which is not an engineering argument and is the real one.
9. RTL 4 — The Discriminator Ranker
// ---------------------------------------------------------------------
// discriminator_ranker -- given the surviving candidates, say which
// observation to make next.
//
// The ranking is by how evenly a counter splits the CURRENT candidate
// set, not the whole space. A counter worth 0.98 bits at the start can
// be worth zero three steps in, because every site it distinguishes
// has already been eliminated.
// ---------------------------------------------------------------------
module discriminator_ranker
import debugm_pkg::*;
(
input logic clk,
input logic rst_n,
input site_mask_t candidates,
input logic [4:0] optional_present,
output logic [3:0] best_counter, // index into the ten
output logic [3:0] best_split, // candidates it would remove
output logic no_useful_question,
output logic [3:0] questions_left
);
site_mask_t masks [10];
logic avail [10];
always_comb begin
masks[0] = M_CRC; avail[0] = 1'b1;
masks[1] = M_ALIGN; avail[1] = 1'b1;
masks[2] = M_UNDERSIZE; avail[2] = 1'b1;
masks[3] = M_OVERSIZE; avail[3] = 1'b1;
masks[4] = M_FRAMES_IN; avail[4] = 1'b1;
masks[5] = M_SYMBOL; avail[5] = optional_present[0];
masks[6] = M_FILTERED; avail[6] = optional_present[1];
masks[7] = M_VLAN_DISC; avail[7] = optional_present[2];
masks[8] = M_FIFO_DROP; avail[8] = optional_present[3];
masks[9] = M_DESC_ERR; avail[9] = optional_present[4];
end
logic [3:0] n_cand, yes_n, best_score;
always_comb begin
n_cand = 4'(popcount12(candidates));
best_counter = 4'd0;
best_score = 4'd0;
questions_left = 4'd0;
for (int i = 0; i < 10; i++) begin
if (avail[i]) begin
yes_n = 4'(popcount12(candidates & masks[i]));
// An even split is the best question. The score is the size of
// the SMALLER half -- maximising it maximises the guaranteed
// reduction, which is the right objective for a worst case.
if (yes_n != 4'd0 && yes_n != n_cand) begin
questions_left = questions_left + 4'd1;
if (((yes_n < (n_cand - yes_n)) ? yes_n : (n_cand - yes_n)) > best_score) begin
best_score = (yes_n < (n_cand - yes_n)) ? yes_n : (n_cand - yes_n);
best_counter = 4'(i);
end
end
end
end
best_split = best_score;
// Section 4's class G: every remaining counter either moves for
// all the candidates or for none of them.
no_useful_question = (questions_left == 4'd0) && (n_cand > 4'd1);
end
endmoduleClassification: a greedy chooser whose objective is the worst case rather than the average.
What it teaches: that the right score is the size of the smaller half. Maximising the expected reduction favours a lopsided question that is usually cheap; maximising the smaller half bounds the number of steps whatever the answer is — and a debugging procedure is judged on its worst case, because the failures that reach an engineer are the ones the easy path did not solve.
And it teaches that no_useful_question is the method's honest terminal state. When every available counter moves for all the surviving candidates or for none of them, the procedure is finished and has not converged. That is class G with the optional counters absent, it is the common case, and Section 20's rejected property is the assertion that it cannot happen.
Deliberately simplified: the ranking ignores the cost of an observation, and the costs differ by three orders of magnitude — a register read is microseconds, an MDIO transaction is milliseconds, and a capture at a different point in the topology is a site visit. A real ranker divides the score by the cost. The for loop over ten masks with a nested population count is combinational and enormous. And best_counter breaks ties by index rather than by cost, so it will send an engineer to MDIO before checking a free local register.
Production implication: the block's most useful output is the one that looks least interesting. questions_left counts the observations that would still narrow something, and when it reaches zero the correct action is to change the instrumentation, not to keep measuring. Teams reliably do the opposite: the tenth measurement on a converged mask feels like progress and Section 22's first complaint is exactly that pattern.
10. What a Zero Rules Out
A counter that moves is a strong statement. A counter reading zero is a weaker one and is not a worthless one, and the difference between those two claims is where most Ethernet debugging goes wrong.
Formally, a zero reading intersects the candidate set with the counter's complement — so it eliminates exactly the sites the counter moves for.
| Counter | A move eliminates | A zero eliminates |
|---|---|---|
c_crc_errors | 7 of 12 | 5 |
frames_in | 8 | 4 |
c_alignment_errors | 9 | 3 |
c_undersize | 9 | 3 |
c_oversize | 10 | 2 |
c_fifo_drops | 11 | 1 |
c_desc_errors | 11 | 1 |
frames_out | 0 | 12 — and it never reads zero |
Both columns of every row sum to twelve, which is the point: a reading eliminates the sites it is inconsistent with, and "moved" and "zero" are inconsistent with complementary sets. The asymmetry is not in the information; it is in which set is bigger.
Three zeros are worth having and the rest are nearly free of content.
| Zero reading | Eliminates | Worth it? |
|---|---|---|
c_crc_errors = 0 | channel, PHY, xMII, parser, CRC logic | yes — five sites, the largest single step |
frames_in = 0 | channel, PHY, xMII, preamble | yes, and it means the frames never arrived |
c_fifo_drops = 0 | the FIFO | only if the counter exists |
Row one is the single most valuable observation in the method and it is a zero. c_crc_errors at zero over a window in which frames were lost eliminates everything from the connector to the check engine — five of twelve sites, 0.98 bits — and it costs one register read. The instinct to go looking for CRC errors is right; the instinct to stop when there are none is exactly wrong.
And there is one reading that is not evidence at all, which Section 3's EV_ABSENT exists for:
| Means | Eliminates | |
|---|---|---|
c_fifo_drops reads 0 | the FIFO did not drop | 1 site |
c_fifo_drops is not implemented | nothing | 0 sites |
| the register reads 0 because it is not implemented | nothing | 0 — and it looks like the first row |
Row three is the trap and it is a hardware fact rather than a methodological one. An unimplemented register in a MAC's statistics block very often reads as zero rather than faulting — so the two readings are byte-identical and the only way to tell them apart is the datasheet. A method that does not check implementation first will confidently eliminate a site on the strength of a register that does not exist, which is the most expensive single error available in this chapter.
A zero eliminates the counter's sites. An absent counter eliminates nothing. On most hardware they read the same, so implementation is a precondition and not a detail.
One more asymmetry, because it decides the order of Section 12's steps.
A moving counter and a zero counter carry the same information in the formal sense — they eliminate complementary sets — but they do not carry the same risk.
| A move | A zero | |
|---|---|---|
| can be produced by an absent counter? | no | yes |
| can be produced by too short a window? | no | yes — Section 18's row nine |
| can be produced by a counter that under-counts? | it still moves | only if it under-counts to zero |
| failure mode | none | eliminates a site wrongly |
A counter that moves is self-validating: something incremented it. A counter at zero is a claim about the absence of an event, and three separate mechanisms produce that reading without the absence being real — the register does not exist, the window was too short, or Chapter 19.7 §7's addend is undersized on exactly this traffic.
Which is why the procedure reads the most informative counter first and treats its zero as the strongest step it will take. c_crc_errors at zero eliminates five sites — the largest single move in the method — and it is also the single reading most worth double-checking, by lengthening the window before acting on it.
11. RTL 5 — The Zero-Evidence Guard
// ---------------------------------------------------------------------
// zero_evidence_guard -- distinguish "this counter read zero" from
// "this counter does not exist", when the register returns zero in
// both cases.
//
// The trick is that an unimplemented counter is zero under conditions
// where an implemented one CANNOT be. If frames are being filtered --
// which a deliberately wrong destination address guarantees -- then a
// c_filtered of zero is proof the counter is not there.
// ---------------------------------------------------------------------
module zero_evidence_guard
import debugm_pkg::*;
(
input logic clk,
input logic rst_n,
// A probe: traffic the environment KNOWS must move each counter.
input logic probe_active,
input logic [2:0] probe_kind, // 0 filtered, 1 vlan, 2 fifo, 3 desc
input logic [31:0] probe_frames_sent,
input logic [31:0] d_filtered,
input logic [31:0] d_vlan_disc,
input logic [31:0] d_fifo_drop,
input logic [31:0] d_desc_err,
output logic [4:0] proven_present,
output logic [4:0] proven_absent,
output logic probe_inconclusive
);
localparam int PROBE_MIN = 1000;
logic enough;
assign enough = probe_active && (probe_frames_sent >= 32'(PROBE_MIN));
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
proven_present <= 5'd0;
proven_absent <= 5'd0;
end else if (enough) begin
case (probe_kind)
3'd0: if (d_filtered != 32'd0) proven_present[1] <= 1'b1;
else proven_absent [1] <= 1'b1;
3'd1: if (d_vlan_disc != 32'd0) proven_present[2] <= 1'b1;
else proven_absent [2] <= 1'b1;
3'd2: if (d_fifo_drop != 32'd0) proven_present[3] <= 1'b1;
else proven_absent [3] <= 1'b1;
default: if (d_desc_err != 32'd0) proven_present[4] <= 1'b1;
else proven_absent [4] <= 1'b1;
endcase
end
end
// Section 12's precondition, as a bit: until every optional counter
// has been probed, a zero reading on any of them is not evidence.
assign probe_inconclusive = ((proven_present | proven_absent) != 5'b11110);
endmoduleClassification: an instrumentation test, which is a different kind of block from everything else in the chapter.
What it teaches: that the way to tell a zero from an absence is to make the counter move on purpose. Send a thousand frames to a destination address the port does not own; Chapter 7.4's filter must discard every one of them. If c_filtered is still zero, the counter is not implemented — and that is now a proven fact rather than an assumption. The probe is Chapter 20.5's error injector used against the instrumentation instead of against the design.
And it teaches that this is a precondition rather than a step. probe_inconclusive should be checked before the first diagnostic observation, because every subsequent conclusion drawn from a zero depends on it — and a procedure that probes after the fact has to discard everything it concluded.
Deliberately simplified: PROBE_MIN is 1 000 frames, which is a guess at how many are needed for a counter to be visibly non-zero and takes no account of a counter that samples. The FIFO probe cannot be performed from the wire at all — Chapter 20.5 §12 proved no sequence of frames overflows a correctly sized FIFO — so probe_kind 2 requires a stalled consumer, which this block silently assumes somebody arranged. And proven_absent is never cleared, so a platform change mid-session is not noticed.
Production implication: the FIFO row is the one that makes this block honest about its limits. Three of the four optional counters can be probed from the wire with one malformed or misaddressed frame each; the fourth cannot be probed from the wire at all. So on a platform where c_fifo_drops reads zero and cannot be provoked, the receive FIFO remains in the candidate set permanently — and Chapter 19.5 §14's truncation path is exactly the failure that most needs localising. The chapter's method is strongest where the design is easiest and weakest where it is hardest, which is worth knowing before an outage rather than during one.
12. The Procedure, Written Down
Eight steps. The first three cost nothing, the next three cost a register read each, and only the last two need equipment.
| Step | Do | Costs | Narrows from 12 to |
|---|---|---|---|
| 0 | read the datasheet: which optional counters exist? | a minute | 12 — but it decides the floor |
| 0b | probe each one — Section 11 | 4 000 frames | 12 |
| 1 | frames_in against frames_out: is there a fault? | a read | 12 — zero bits, Section 6 |
| 2 | the conservation identity — Section 7 | six reads | 8 or 4 |
| 3 | c_crc_errors | a read | 5 or 3 |
| 4 | c_alignment_errors and c_undersize | two reads | 1 to 3 |
| 5 | the optional counters, in Section 8's order | a read each | 1, if they exist |
| 6 | only now: a capture or a waveform | hours | — |
Step 6's position is the whole argument for writing the procedure down. Every step above it is a register read, and together they take a candidate space of twelve to a single site on a fully instrumented platform or to a class of five otherwise. A capture answers one question and takes three hours; a register read answers a question worth 0.98 bits and takes a millisecond.
The steps in order, with what each one is actually asking.
Step 0 — implementation. Four questions of a datasheet. The answers decide whether the investigation can end with a site or with a shortlist, and they are free.
Step 0b — the probe. Three of the four optional counters can be made to move deliberately. Do it once per platform, not once per outage — Section 11.
Step 1 — the trigger. frames_out short of frames_in confirms a fault and eliminates nothing. Its value is that it bounds the window: the counters read from here are deltas, and Section 18's row three is about how long that window should be.
Step 2 — conservation. Does the arithmetic balance? Chapter 20.3 §13's identity, with Chapter 19.1 §6's twelve-frame skew allowance. It balances → the missing frames are accounted for by a named counter. It does not → they vanished where nothing counts, which is four sites.
Step 3 — the best single question. c_crc_errors splits five from seven — the most even split any counter offers. Moving points at the channel, the PHY, the xMII, the parser or the CRC logic; zero eliminates all five.
Step 4 — the separators. c_alignment_errors separates the physical sites from the logical ones; c_undersize and c_oversize separate the parser from the validity classifier, which is the one pair in the space that is genuinely easy to confuse and genuinely easy to separate.
Step 5 — the linear tail. Four counters, four sites, one at a time. Section 8's arithmetic says this cannot be improved with these counters.
Step 6 — equipment. By now the candidate set is a site or a class of five, and a capture taken at step 6 is aimed — at one interface, for one condition, with an oracle. A capture taken at step 1 is a fishing expedition and Chapter 21.9 is about what it can and cannot show.
And the procedure's own stopping rule, which is the part people leave out:
Stop when
questions_leftis zero. If the mask is a single site, that is the answer. If it is a class, the answer is "these five, and here is the counter that would separate them" — and that is a finished diagnosis, not a failed one.
| Terminal state | Report | Next action |
|---|---|---|
| one site | the site | fix it |
| class G — five sites | the class, and which counter is missing | instrumentation, not investigation |
| class B — two sites | PHY or xMII | read the PHY over Chapter 4.5's MDIO |
| empty mask | the model is wrong | Section 22's third complaint |
And there is a variant of the procedure worth naming, because it is what a monitoring system should run rather than what a person runs.
Every step up to 5 is a register read, so the whole thing can be executed continuously on every port in a fleet, at no cost, with nobody watching.
| Run by a person, during an outage | Run by a monitor, continuously | |
|---|---|---|
| step 0 | once, and usually skipped | once per platform, recorded |
| step 0b | rarely done | at commissioning |
| steps 1 to 5 | eleven reads, by hand | eleven reads, every minute |
| what it produces | an answer, after an hour | a candidate mask, already narrowed |
best_possible_answer | discovered late | a fleet-wide inventory |
Row five is the one that changes how a team spends its time. site_unreachable is a property of the port, it never changes, and knowing it for every port before anybody is paged turns "this link is dropping frames" into either a one-hour investigation or a four-hour one, decided in advance. The reads cost nothing and the inventory is built once.
Row four is the state nobody plans for and it happens. An empty candidate mask means two observations contradicted each other under Section 2's table — which is not a design fault and not an instrumentation fault. It is a fault in the table, and the correct response is to find which pair of observations is inconsistent rather than to keep measuring.
13. RTL 6 — The Evidence Recorder
// ---------------------------------------------------------------------
// evidence_recorder -- what was observed, in what order, and what each
// observation ruled out.
//
// The output that matters is not the conclusion. It is the AUDIT: a
// diagnosis that cannot say which observation eliminated which site
// cannot be reviewed, and an unreviewable diagnosis is an opinion.
// ---------------------------------------------------------------------
module evidence_recorder
import debugm_pkg::*;
#(
parameter int MAX_STEPS = 16
)(
input logic clk,
input logic rst_n,
input logic restart,
input logic obs_valid,
input logic [3:0] obs_counter, // which of the ten
input evidence_e obs_kind,
input site_mask_t before_mask,
input site_mask_t after_mask,
output logic [3:0] n_steps,
output site_mask_t eliminated_by [MAX_STEPS],
output logic [3:0] counter_of [MAX_STEPS],
output logic [3:0] wasted_steps,
output logic contradiction
);
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n || restart) begin
n_steps <= 4'd0;
wasted_steps <= 4'd0;
contradiction <= 1'b0;
for (int i = 0; i < MAX_STEPS; i++) begin
eliminated_by[i] <= 12'd0;
counter_of[i] <= 4'd0;
end
end else if (obs_valid && n_steps < 4'(MAX_STEPS)) begin
eliminated_by[n_steps] <= before_mask & ~after_mask;
counter_of[n_steps] <= obs_counter;
n_steps <= n_steps + 4'd1;
// An observation that eliminated nothing. Section 9's ranker
// should have prevented it; recording it is how a team finds
// out the ranker was bypassed.
if ((before_mask & ~after_mask) == 12'd0)
wasted_steps <= wasted_steps + 4'd1;
// Section 12's row four: two readings that cannot both be true
// under Section 2's table.
if (after_mask == 12'd0) contradiction <= 1'b1;
end
end
endmoduleClassification: a log, and the only block in the chapter whose value is entirely retrospective.
What it teaches: that eliminated_by is the field that makes a diagnosis reviewable. "It is the DMA" is an opinion; "c_crc_errors was zero, which eliminated five sites; the conservation identity held, which eliminated four more; c_filtered and c_vlan_discards were zero, which eliminated two" is an argument — and the difference is that the second one can be checked by somebody who was not there.
And it teaches that wasted_steps is a measurement of the procedure rather than of the fault. A step that eliminated nothing was a question whose answer was already known, and Section 9's ranker exists to prevent exactly that. A post-mortem with three wasted steps out of seven has found a process problem, and it is a process problem that costs about three hours per outage.
Deliberately simplified: the recorder stores MAX_STEPS masks in flops — 16 × 12 bits plus 16 × 4 is 256 flops — where a real one writes to a log. There is no timestamp, so the record cannot say how long each step took, which is the other number a post-mortem wants. And contradiction is set and never explained: the block knows the mask went empty and not which earlier observation the last one contradicted.
Production implication: the contradiction case is worth instrumenting properly, because it is the one that indicts Section 2's table. When a mask goes empty, one of the ten mask constants is wrong for this design — a counter that moves for a site the table says it does not, or the reverse. Storing before_mask for every step makes it a five-minute search; storing only the conclusion makes it unfindable, and the table then stays wrong for every future investigation.
14. What a Debugging Method Must Never Do
Six prohibitions. Three are about evidence and three are about stopping, and every one of them produces a confident wrong answer rather than a visible failure.
| Never | Because | |
|---|---|---|
| 1 | treat an absent counter as a zero | Section 10 — on most hardware they read the same |
| 2 | conclude a site from a symptom | c_crc_errors moving is consistent with five sites |
| 3 | take a capture before step 5 | Section 12 — three hours for less than a bit |
| 4 | report a class as a site | Section 4's class G has five members |
| 5 | keep measuring when questions_left is zero | Section 9 — the instrumentation is the blocker |
| 6 | discard the evidence log after the fix | Section 13 — the table stays wrong |
Row two is the one that is violated hourly and it has a name in this track. "CRC errors are climbing, so the cable is bad" is Chapter 11.4 §15's class 48 — a symptom implying a cause where the mapping is many-to-many — arriving as an operational habit rather than as an assertion. It is right about five twelfths of the time and it is the reason cables get replaced on links whose fault is in the parser's offset arithmetic.
Row four is the one that damages a team's credibility. Reporting "the fault is in the DMA" when the evidence supports "the filter, VLAN, the FIFO, the DMA or the driver" is a one-in-five guess dressed as a conclusion. When it is wrong, the next investigation starts from a discredited method — and the honest report costs nothing: five candidates, and c_fifo_drops would separate them.
And the two that look like impatience and are not:
| Why it is a prohibition | |
|---|---|
| row three | a capture is worth about one bit and costs three hours |
| row five | more measurement of the same counters adds exactly zero |
Both produce activity that looks like progress and is not, which is the property all six share and is why a procedure needs a stopping rule written down before the outage rather than during it.
15. RTL 7 — Debug Telemetry
// ---------------------------------------------------------------------
// debugm_telemetry -- how well the method is doing, which is a
// different question from what the fault is.
//
// Two groups. "Progress" says whether the procedure is still
// narrowing. "Ceiling" says how good an answer this platform can
// produce at all -- and the second is knowable before the first step.
// ---------------------------------------------------------------------
module debugm_telemetry
import debugm_pkg::*;
(
input logic clk,
input logic rst_n,
input site_mask_t candidates,
input logic [3:0] n_observations,
input logic [3:0] wasted_steps,
input logic [4:0] optional_present,
input logic probe_inconclusive,
input logic contradiction,
input logic no_useful_question,
// Progress.
output logic [3:0] n_candidates,
output logic [15:0] efficiency_pct,
output logic stalled,
// Ceiling -- all knowable at step 0.
output logic [3:0] classes_available,
output logic [3:0] largest_class,
output logic [3:0] best_possible_answer,
output logic site_unreachable
);
localparam int OPTIMAL_STEPS = 7;
assign n_candidates = 4'(popcount12(candidates));
// What fraction of the observations made were useful.
always_comb
efficiency_pct = (n_observations == 4'd0) ? 16'd100
: 16'(((32'(n_observations) - 32'(wasted_steps)) * 32'd100)
/ 32'(n_observations));
assign stalled = no_useful_question || contradiction;
// Section 4's table as arithmetic: seven classes with the required
// six, and one more class per optional counter present.
always_comb begin
classes_available = 4'd7;
for (int i = 0; i < 5; i++)
if (optional_present[i]) classes_available = classes_available + 4'd1;
// The silent class shrinks by one per NON-symbol optional counter.
largest_class = 4'd5;
for (int i = 1; i < 5; i++)
if (optional_present[i] && largest_class > 4'd1)
largest_class = largest_class - 4'd1;
end
assign best_possible_answer = largest_class;
// A platform on which the answer can never be a single site.
assign site_unreachable = (largest_class > 4'd1) || probe_inconclusive;
endmoduleClassification: a reporting block whose second group of outputs does not depend on any observation at all.
What it teaches: that best_possible_answer is computable at step 0 — from a datasheet, before any traffic — and that it is the single most useful thing to tell a team at the start of an investigation. "On this platform the best answer available is a class of three" changes the plan; discovering it after four hours changes only the mood.
And it teaches that efficiency_pct measures the procedure and n_candidates measures the fault. They are independent: a procedure can be 100% efficient and still finish with five candidates, which is the platform's fault, and it can be 40% efficient and finish with one, which is luck. Reporting only the second is how a bad method survives a good outcome.
Deliberately simplified: largest_class decrements once per optional counter without checking which — so a platform with c_symbol_errors and nothing else gets credit it has not earned, since the symbol counter splits class B and not class G. The loop starts at index 1 to exclude it, which is correct and is a hardcoded fact about Section 4's table. OPTIMAL_STEPS is declared and unused, which is where an efficiency figure against the seven-step optimum should go.
Production implication: site_unreachable is the bit that should appear in a monitoring system rather than in a debug session. It is a property of the platform, it never changes, and knowing it for every port in a fleet turns "this link is dropping frames" into either a four-hour investigation or a one-hour one before anybody is paged. Chapter 19.7 §15's telemetry made the same argument about the design; this is the same argument about the instrumentation around it.
16. RTL 8 — The Debug Conformance Monitor
// ---------------------------------------------------------------------
// debugm_conformance_monitor -- six verdicts about the METHOD, and
// four of them fire on a method that is being followed correctly.
//
// That ratio is the chapter's point. Most of what limits an Ethernet
// diagnosis is not the investigator; it is a counter that does not
// exist, and no amount of care substitutes for it.
// ---------------------------------------------------------------------
module debugm_conformance_monitor
import debugm_pkg::*;
(
input logic clk,
input logic rst_n,
input site_mask_t candidates,
input logic [3:0] n_observations,
input logic [3:0] wasted_steps,
input logic [4:0] optional_present,
input logic probe_inconclusive,
input logic contradiction,
input logic capture_taken,
input logic [3:0] capture_at_step,
output logic instrumentation_limited,
output logic probe_skipped,
output logic capture_premature,
output logic steps_wasted,
output logic table_wrong,
output logic method_exhausted,
output logic method_sound
);
localparam int CAPTURE_MIN_STEP = 5;
// Section 4: the four non-symbol optional counters are what shrink
// class G, and their absence is a platform fact.
assign instrumentation_limited = (optional_present[4:1] != 4'b1111);
// Section 11: a zero reading is not evidence until the counter is
// proven to exist.
assign probe_skipped = probe_inconclusive && (n_observations > 4'd0);
// Section 12's row three. A capture before the register reads are
// exhausted costs hours and is worth about a bit.
assign capture_premature = capture_taken &&
(capture_at_step < 4'(CAPTURE_MIN_STEP));
assign steps_wasted = (wasted_steps > 4'd0);
// Section 13: an empty mask means two observations contradicted each
// other under Section 2's table, which indicts the table.
assign table_wrong = contradiction;
assign method_exhausted = (popcount12(candidates) > 1) &&
(n_observations >= 4'd7);
assign method_sound = !probe_skipped && !capture_premature &&
!steps_wasted && !table_wrong;
always_ff @(posedge clk) begin
if (rst_n && instrumentation_limited)
$display("[debug] best possible answer on this platform is a class, not a site");
end
endmoduleClassification: an auditor of a procedure, whose most-fired verdict is not about the procedure.
What it teaches: that instrumentation_limited is deliberately not a term of method_sound. A platform missing three optional counters does not make the method unsound — the method is doing everything available correctly and the answer is a class. Folding a platform limitation into a verdict about the investigator's work is how a team learns to distrust its own process for a reason that has nothing to do with it.
And it teaches why method_exhausted is separate from stalled. Section 15's stalled means no question remains; method_exhausted means seven observations have been made and more than one candidate survives — which, by Section 8's arithmetic, should be impossible on a fully instrumented platform. When both are false and the count is above seven, the observations are being wasted, and steps_wasted says so.
Deliberately simplified: CAPTURE_MIN_STEP is 5, which is a judgement and is wrong for a fault whose symptom is a malformed frame — there, a capture at step 1 is the correct first move because the frame's contents are the evidence. The monitor has no notion of symptom class, which Chapter 21.2 supplies. And $display fires every cycle that the condition holds, which is deliberate for the same reason as elsewhere in the track: a silence reads as a pass.
Production implication: probe_skipped is the verdict worth wiring into a team's tooling rather than into a report. It fires the moment somebody draws a conclusion from a counter whose existence has not been established — and Section 10 showed that an unimplemented counter and a genuinely zero one read identically. One bit, checked before the first conclusion, prevents the most expensive single error in the chapter, which is eliminating the site the fault is actually at.
17. Where the Method Stops
The method's output is a mask. This section is about the four ways that mask fails to be a single site, and which of them are the investigator's problem.
| Terminal state | Cause | Whose problem | Frequency |
|---|---|---|---|
| a single site | full instrumentation, sound method | — | only with all eleven counters |
| class G — five sites | the four optional counters absent | the platform | the common case |
| class B — two sites | no PHY symbol counter over MDIO | the platform | common |
| an empty mask | Section 2's table is wrong for this design | the table | rare and important |
| more than seven observations | the ranker was bypassed | the method | avoidable |
And one terminal state that is not in the table because it is not a state of the mask.
The procedure can finish correctly, name a single site, and be wrong — when the fault is not in the space at all. A transmit-side fault at the far end, a misconfigured switch three hops away, an MTU mismatch that drops only large frames: none of the twelve sites is faulty and the method will still converge, because it is an intersection over a table that has no row for "none of the above."
| Detectable by the mask? | Detectable at all? | |
|---|---|---|
| a fault outside the twelve | no | only by the conservation identity balancing |
| a transmit fault | no | Section 18's row six |
| a size-dependent drop | no | the counters are not conditioned on size |
Row three is the one worth knowing about because it has a cheap tell. An MTU mismatch drops only frames above a threshold, so frames_out is short and c_oversize never moves — which looks exactly like class G. The distinguishing evidence is not a counter at all: it is that the loss rate is a function of the frame size, and no register in the RMON set is conditioned on size. Chapter 19.7 §2's histogram is the nearest thing, and comparing its transmit and receive shapes is the one observation that separates a size-dependent drop from an unconditional one.
Rows two and three cover seven of the twelve sites — 58.3% — and neither is anybody's mistake. They are what the RMON standard's scope costs: the standard specifies error counters, and the five sites in class G do not produce errors.
And there is a fifth state the mask cannot represent at all.
| The method assumes | When it is false | |
|---|---|---|
| one fault | one site is faulty | two faults mask each other's evidence |
| a stationary fault | the counters describe the same fault | an intermittent link changes between reads |
| a receive fault | the space is the receive path | a transmit fault presents as twelve wrong candidates |
Row one is the assumption whose failure is hardest to see. Two simultaneous faults do not produce the union of their signatures — they produce the signature of whichever one the frame meets first, because a frame discarded at the parser never reaches the FIFO. So the candidate mask converges, confidently, on the earlier of the two sites; the fix is applied; the symptom persists; and the second investigation starts from scratch with the first one's conclusion now looking like an error.
The detection is available and it is not in the mask. Chapter 20.3 §13's conservation identity is the one observation that sees all the loss at once:
| One fault | Two faults | |
|---|---|---|
| candidate mask | converges | converges — on the earlier site |
unaccounted after the fix | zero | still non-zero |
| what to do | close it | restart with the mask reset |
Row two is the check to run after every fix and before closing anything, and it costs six register reads. A fix that eliminates the symptom and leaves unaccounted non-zero has not finished — and that is the cheapest available protection against the second-fault case, which is otherwise indistinguishable from a bad diagnosis.
And the masking is not symmetric, which is worth one more table because it says which pairs are dangerous.
A frame meets the twelve sites in order. So a fault at site i masks a fault at site j whenever i is less than j and the earlier fault discards the frame — and every one of the twelve except the CRC-logic case does discard it.
| Earlier fault | Masks | Of the twelve |
|---|---|---|
| 1 channel | sites 2 to 12 | 11 |
| 4 preamble | sites 5 to 12 | 8 |
| 7 validity | sites 8 to 12 | 5 |
| 10 FIFO | sites 11 and 12 | 2 |
| 12 driver | nothing | 0 |
Summed over all twelve sites, 66 of the 132 ordered pairs are masking pairs — exactly half. So if two independent faults are present, there is a 50% chance the method sees only one of them, and it sees the earlier one with certainty rather than at random.
| Value | |
|---|---|
| ordered pairs of distinct sites | 132 |
| pairs where the earlier masks the later | 66 |
| share | 50.0% |
| which one the mask converges on | always the earlier |
The "always the earlier" row is what makes the case tractable. A masked second fault is not a random error; it is systematically downstream of the one that was found — so the re-check after a fix has a direction: look at the sites after the one you just fixed, and the conservation identity says whether you need to. That is a much better position than "the fix did not work", and it costs the same six reads.
18. What the Method Assumes
Nine assumptions. Three are about the counters, three about the fault and three about the design — and the one most likely to be false is about the window.
| Assumption | From | If false | |
|---|---|---|---|
| 1 | a counter that exists is correct | Chapter 19.7 | Section 2's table is a fiction |
| 2 | an unimplemented counter reads zero | hardware convention | it faults, which is better |
| 3 | the deltas cover one window | the reader | Section 18's row nine |
| 4 | exactly one site is faulty | convenience | Section 17 — the mask converges on the earlier |
| 5 | the fault is stationary | convenience | an intermittent link changes between reads |
| 6 | the fault is on the receive path | scope | twelve wrong candidates |
| 7 | Section 2's table matches this design | Modules 5 to 19 | an empty mask — Section 13 |
| 8 | the counters commit within 12 frames | Chapter 19.1 §6 | the conservation identity mis-fires |
| 9 | the window is long enough to see a rare fault | nothing at all | a zero that is not a zero |
Row nine is the assumption nothing in this chapter supports and it deserves its own arithmetic. A fault that corrupts one frame in ten million is a real and common failure mode — a marginal connector, a lane at the edge of its eye. At 100 Gb/s and minimum size, 148.9 million frames per second means one corruption every 67 milliseconds, which any window catches. At 1 Gb/s the same rate is 1.488 million frames per second and one corruption every 6.7 seconds — so a one-second window reads c_crc_errors as zero and eliminates five sites that should have stayed.
| Line rate | Frames per second, minimum size | A 1-in-10⁷ fault appears every | A 1 s window sees |
|---|---|---|---|
| 1 Gb/s | 1.488 M | 6.7 s | 0 or 1 — usually 0 |
| 10 Gb/s | 14.88 M | 672 ms | about 1 |
| 100 Gb/s | 148.9 M | 67 ms | about 15 |
The same fault, the same counter, and the same window produce a confident elimination at 1 Gb/s and a clear detection at 100. Which gives the rule the method needs and does not have:
A zero is only evidence if the window was long enough for the rarest fault you care about. At 1 Gb/s that is tens of seconds; at 100 Gb/s it is tens of milliseconds.
Row one is the assumption whose failure is the most embarrassing. Chapter 19.7 §7 showed a counter sized for conformant traffic counts exactly half of a 47-octet runt flood, because its addend is one bit where two are needed. A method reading such a counter gets a number that is wrong by a clean fraction — and a clean fraction reads as a real measurement rather than as a bug.
And three things deliberately not assumed:
| Not assumed | Why not |
|---|---|
| that a symptom implies a site | Section 2's table is many-to-many — Chapter 11.4 §15's class 48 |
| that a converged mask is correct | Section 17's two-fault case converges too |
| that the investigator has a capture | Section 12 — five of the eight steps are register reads |
Row three is the chapter's practical thesis. Everything up to step 5 is a register read, which means the whole method can be run from a script, from a monitoring system, or by somebody with no equipment and a serial console — and it reaches a site or a class of five before anybody unpacks a capture tool.
19. The Cost, Accounted
A debugging method is not hardware, so the accounting here is of two different things: what the method's own machinery costs if you build it, and what the instrumentation it wants costs in the design.
| Block | Flops | Nature |
|---|---|---|
debugm_pkg | 0 | one table |
candidate_mask | ~21 | 12 bits of mask and two counters |
symptom_classifier | 0 | combinational |
conservation_checker | 0 | combinational |
discriminator_ranker | 0 | combinational |
zero_evidence_guard | ~10 | two 5-bit results |
evidence_recorder — 16 × 16 bits | ~268 | the audit log |
debugm_telemetry | 0 | combinational |
debugm_conformance_monitor | 0 | combinational |
| total | ~299 flops |
Two hundred and ninety-nine flops is the cheapest structure in Modules 20 and 21, and the reason is that a decision procedure over twelve candidates has almost no state: twelve bits of mask, a step count, and a log. Chapter 20.6 §19's join table was 74 017 flops because it remembers something per frame; this remembers something per observation, and there are seven of those.
The instrumentation is the real cost and it is in the design.
| Flops | Buys | |
|---|---|---|
| the RMON six | already there — Chapter 19.7 | 7 classes, largest 5 |
c_filtered | ~99 | splits one site off class G |
c_vlan_discards | ~99 | one more |
c_fifo_drops | ~99 | one more |
c_desc_errors | in the driver — 0 gates | one more |
| the four optional | ~297 | 12 classes, largest 1 |
| twelve boundary counters — Section 8 | ~1 188 | 4 observations instead of 7 |
Two hundred and ninety-seven flops — 2.1% of Chapter 19.7 §19's 14 166-flop datapath — is the difference between a diagnosis that names a site and one that names five. That is the whole economic content of this chapter, and it is a smaller number than any single mechanism Module 19 argued about.
| Flops | Share of the datapath | |
|---|---|---|
| Chapter 19.4's correction barrels | 5 397 XOR terms | the largest single mechanism |
| Chapter 19.7's shadow bank | 992 | 7.0% |
| the four optional counters | ~297 | 2.1% |
| twelve boundary counters | ~1 188 | 8.4% |
Row three is 2.1% and it is not spent, on most parts. Row two is 7.0% and it is spent, because a common reading instant is a correctness requirement and a debuggable fault space is not. Nothing in the standard requires a counter for a non-error, which is the whole reason class G exists — and Section 4's table is what the omission costs, expressed as five candidates instead of one.
And a last comparison, because it is the one that persuades:
| Cost | |
|---|---|
| four optional counters | ~297 flops, once, at design time |
| one investigation that ends at class G | a capture, three hours, and a one-in-five guess |
There is a second cost this chapter's method imposes that is not flops at all, and it is the one a team actually feels.
| Cost per investigation | |
|---|---|
| step 0 — four datasheet questions | one minute, once per platform ever |
| step 0b — the probe | 4 000 frames, once per platform ever |
| steps 1 to 5 — eleven register reads | milliseconds, scriptable |
| step 6 — a capture | three hours, and only if steps 1 to 5 left a class |
Rows one and two are amortised over a platform's whole life and rows three is amortised over nothing because it is free. The entire method costs one minute, once, and then milliseconds per outage — against three hours for the single observation it is designed to postpone.
Which produces the comparison worth putting in front of whoever decides the 2.1%:
| A port without the four counters | With them | |
|---|---|---|
| terminal state | five candidates | one site |
| captures per outage | at least one | often none |
| expected wrong fixes | 0.8 — four in five | 0 |
| cost, once | 0 flops | ~297 flops |
Row three is the number that does not appear on any design review slide. A class of five resolved by guessing is wrong four times in five, and each wrong fix is a maintenance window, a change record and a second investigation that starts from a discredited method. Two hundred and ninety-seven flops is about three tenths of what Chapter 19.7's shadow bank costs, and the shadow bank was built without argument because a common reading instant is a correctness requirement. Debuggability is not, which is the entire reason this section exists.
20. Properties Worth Asserting, and One Worth Refusing
Thirty-three properties and eight covers, in four groups: the mask's algebra, the evidence's three-valuedness, the procedure's progress, and the terminal states.
Group one — the mask.
// A candidate set only ever shrinks, and an observation that grows it
// is a table error rather than a new hypothesis.
p_mask_monotone: assert property (@(posedge clk) disable iff (!rst_n)
obs_valid |=> ((candidates & $past(candidates)) == candidates));
p_mask_starts_full: assert property (@(posedge clk) disable iff (!rst_n)
restart |=> (candidates == ALL_SITES));
p_mask_intersect: assert property (@(posedge clk) disable iff (!rst_n)
(obs_valid && obs_kind != EV_ABSENT) |=>
(candidates == ($past(candidates) & $past(obs_consistent))));
p_count_matches: assert property (@(posedge clk) disable iff (!rst_n)
n_candidates == 4'(popcount12(candidates)));
p_converged_is_one: assert property (@(posedge clk) disable iff (!rst_n)
converged |-> (n_candidates == 4'd1));
p_classg_is_five: assert property (@(posedge clk) disable iff (!rst_n)
class_only |-> (n_candidates == 4'd5));
p_no_regrowth: assert property (@(posedge clk) disable iff (!rst_n)
(n_candidates <= $past(n_candidates)) || restart);
p_empty_is_flagged: assert property (@(posedge clk) disable iff (!rst_n)
(candidates == 12'd0) |-> contradiction);Group two — evidence, and its three values. Every property here exists because EV_ABSENT is not EV_ZERO.
p_absent_no_narrow: assert property (@(posedge clk) disable iff (!rst_n)
(obs_valid && obs_kind == EV_ABSENT) |=>
(candidates == $past(candidates)));
p_absent_no_step: assert property (@(posedge clk) disable iff (!rst_n)
(obs_valid && obs_kind == EV_ABSENT) |=>
(n_observations == $past(n_observations)));
p_zero_narrows: assert property (@(posedge clk) disable iff (!rst_n)
(obs_valid && obs_kind == EV_ZERO &&
(($past(candidates) & obs_consistent) != $past(candidates)))
|=> (n_candidates < $past(n_candidates)));
p_probe_before_use: assert property (@(posedge clk) disable iff (!rst_n)
(obs_kind == EV_ZERO && !optional_proven) |-> probe_skipped);
p_probe_is_sticky: assert property (@(posedge clk) disable iff (!rst_n)
proven_present[1] |=> always proven_present[1]);
p_probe_exclusive: assert property (@(posedge clk) disable iff (!rst_n)
!(proven_present & proven_absent));
p_frames_out_unused:assert property (@(posedge clk) disable iff (!rst_n)
consistent_valid |-> (consistent == m_without_frames_out));
p_silent_is_classg: assert property (@(posedge clk) disable iff (!rst_n)
symptom_is_silent |-> ((consistent & CLASS_SILENT) != 12'd0));Group three — progress.
p_ranker_narrows: assert property (@(posedge clk) disable iff (!rst_n)
(!no_useful_question && obs_valid &&
obs_counter == $past(best_counter)) |=>
(n_candidates < $past(n_candidates)));
p_best_split_valid: assert property (@(posedge clk) disable iff (!rst_n)
(best_split > 4'd0) |-> !no_useful_question);
p_no_question_stuck:assert property (@(posedge clk) disable iff (!rst_n)
no_useful_question |=> $stable(candidates));
p_wasted_counted: assert property (@(posedge clk) disable iff (!rst_n)
(obs_valid && $stable(candidates) && obs_kind != EV_ABSENT)
|=> (wasted_steps == $past(wasted_steps) + 4'd1));
p_efficiency_range: assert property (@(posedge clk) disable iff (!rst_n)
efficiency_pct <= 16'd100);
p_seven_suffices: assert property (@(posedge clk) disable iff (!rst_n)
((optional_present == 5'b11111) && (n_observations >= 4'd7) &&
(wasted_steps == 4'd0)) |-> converged);
p_conservation_cut: assert property (@(posedge clk) disable iff (!rst_n)
(!identity_undecidable && identity_holds) |->
((cut_mask & 12'b1101_0000_1000) == 12'd0));
p_undecidable_full: assert property (@(posedge clk) disable iff (!rst_n)
identity_undecidable |-> (cut_mask == ALL_SITES));
p_skew_allowance: assert property (@(posedge clk) disable iff (!rst_n)
(unaccounted <= skew_bound) |-> identity_holds);Group four — terminal states, and the ceiling.
p_ceiling_static: assert property (@(posedge clk) disable iff (!rst_n)
$stable(optional_present) |=> $stable(best_possible_answer));
p_site_needs_all: assert property (@(posedge clk) disable iff (!rst_n)
(optional_present[4:1] != 4'b1111) |-> site_unreachable);
p_limited_not_fault:assert property (@(posedge clk) disable iff (!rst_n)
instrumentation_limited |-> !(method_sound == 1'b0 &&
!probe_skipped && !steps_wasted));
p_capture_late: assert property (@(posedge clk) disable iff (!rst_n)
(capture_taken && capture_at_step >= 4'd5) |-> !capture_premature);
p_exhausted_needs7: assert property (@(posedge clk) disable iff (!rst_n)
method_exhausted |-> (n_observations >= 4'd7));
p_sound_excludes: assert property (@(posedge clk) disable iff (!rst_n)
method_sound |-> (!table_wrong && !capture_premature));
p_table_wrong_stick:assert property (@(posedge clk) disable iff (!rst_n)
table_wrong |=> always table_wrong);
p_log_records_all: assert property (@(posedge clk) disable iff (!rst_n)
(obs_valid && n_steps < 4'd16) |=>
(n_steps == $past(n_steps) + 4'd1));
p_log_elimination: assert property (@(posedge clk) disable iff (!rst_n)
(obs_valid && n_steps < 4'd16) |=>
(eliminated_by[$past(n_steps)] ==
($past(before_mask) & ~$past(after_mask))));And eight covers, because six of these are states a reviewer wants to have seen rather than to be promised.
c_reached_site: cover property (@(posedge clk) converged);
c_reached_classg: cover property (@(posedge clk) class_only);
c_contradiction: cover property (@(posedge clk) contradiction);
c_absent_counter: cover property (@(posedge clk) obs_kind == EV_ABSENT);
c_probe_absent: cover property (@(posedge clk) proven_absent != 5'd0);
c_identity_fails: cover property (@(posedge clk) !identity_holds && !identity_undecidable);
c_seven_steps: cover property (@(posedge clk) n_observations == 4'd7);
c_wasted_step: cover property (@(posedge clk) wasted_steps > 4'd0);21. Verification Scenarios
Fifty-eight scenarios for a procedure, plus a five-run directed test whose content is an instrumentation change rather than a stimulus one.
The mask algebra — 10 scenarios.
| # | Scenario | Expected |
|---|---|---|
| 1 | reset | all 12 bits set |
| 2 | c_crc_errors moved | 5 candidates: 0, 1, 2, 4, 5 |
| 3 | c_crc_errors zero | 7 candidates: the complement |
| 4 | crc zero then align zero | still 7 — align's sites already gone |
| 5 | narrowed_nothing on that second step | asserts |
| 6 | crc moved then align moved | 3 candidates: 0, 1, 2 |
| 7 | then c_undersize moved | 1 candidate: the channel |
| 8 | converged at that point | asserts |
| 9 | crc moved then crc zero | empty mask; contradiction |
| 10 | restart after that | 12 bits again; contradiction sticky |
Evidence and its three values — 11 scenarios.
| # | Scenario | Expected |
|---|---|---|
| 11 | c_filtered = 0, counter present | site 7 eliminated |
| 12 | c_filtered = 0, counter absent | nothing eliminated |
| 13 | the two above, same register read | identical bus traffic |
| 14 | 1 000 misaddressed frames, c_filtered moves | proven_present[1] |
| 15 | the same, c_filtered stays zero | proven_absent[1] |
| 16 | 999 frames | inconclusive — below PROBE_MIN |
| 17 | a zero used before the probe | probe_skipped |
| 18 | a FIFO probe from the wire | impossible — Chapter 20.5 §12 |
| 19 | a FIFO probe with a stalled consumer | conclusive |
| 20 | EV_ABSENT on every optional counter | n_observations stays at 5 |
| 21 | probe_inconclusive at the start | asserts until all four are probed |
The conservation cut — 9 scenarios.
| # | Scenario | Expected |
|---|---|---|
| 22 | in − out equals the error sum | identity_holds; 8 candidates |
| 23 | in − out exceeds it by 40 | identity fails; 4 candidates |
| 24 | in − out exceeds it by 8 | within the 12-frame skew; holds |
| 25 | c_filtered absent | identity_undecidable; 12 candidates |
| 26 | the same, reported as a failure | the bug Section 7 exists to prevent |
| 27 | frames_out exceeds frames_in | underflow — a counter wrapped |
| 28 | a 1 Gb/s port with skew_bound = 12 | too generous by 63× — §8's constant |
| 29 | all four optional counters present | the identity is decidable every window |
| 30 | two faults, identity checked after the fix | unaccounted still non-zero |
The ranker and progress — 10 scenarios.
| # | Scenario | Expected |
|---|---|---|
| 31 | 12 candidates, all counters | best is c_crc_errors, split 5 |
| 32 | 5 candidates in class G, RMON only | no_useful_question |
| 33 | the same with c_filtered | best is c_filtered, split 1 |
| 34 | 2 candidates, sites 1 and 2 | best is c_symbol_errors |
| 35 | the same without it | no_useful_question |
| 36 | a question asked twice | wasted_steps increments |
| 37 | seven observations, full instrumentation | converged |
| 38 | eight observations, still 3 candidates | method_exhausted |
| 39 | efficiency_pct with 2 wasted of 7 | 71 |
| 40 | ties between two equal-split counters | broken by index, not by cost |
Terminal states and the ceiling — 10 scenarios.
| # | Scenario | Expected |
|---|---|---|
| 41 | RMON six only | best_possible_answer = 5 at step 0 |
| 42 | RMON plus c_symbol_errors | still 5 — class G untouched |
| 43 | RMON plus the four non-symbol | 1 |
| 44 | site_unreachable on the RMON six | asserts before any observation |
| 45 | a capture at step 1 | capture_premature |
| 46 | a capture at step 6 | not premature |
| 47 | an empty mask | table_wrong, sticky |
| 48 | instrumentation_limited and no wasted steps | method_sound stays high |
| 49 | a converged mask after 4 observations | lucky, not optimal — Chapter 20.4 §18's reading |
| 50 | the evidence log after a 7-step run | 7 non-zero eliminated_by entries |
The window — 8 scenarios.
| # | Scenario | Expected |
|---|---|---|
| 51 | 1-in-10⁷ fault, 100 Gb/s, 1 s window | about 15 CRC errors — detected |
| 52 | the same at 10 Gb/s | about 1.5 — marginal |
| 53 | the same at 1 Gb/s | 0 or 1 — usually eliminated wrongly |
| 54 | 1 Gb/s with a 60 s window | about 9 — detected |
| 55 | a counter that samples rather than counts | the window means nothing |
| 56 | a read-to-clear counter read twice | the second read is a delta of a delta |
| 57 | Chapter 19.7 §7's one-bit addend, runt flood | the count is exactly half |
| 58 | a count that is a clean fraction | the most identifiable counter bug there is |
And the directed test, because random stimulus will not produce it.
The case: one fault, one design, five platforms — and the answer gets better without anybody touching the procedure.
No amount of stimulus reaches this. The variable is which counters the platform implements, which is a synthesis-time or a vendor decision, not a field of anything a generator can randomise. The only way to reach it is to run the same fault against five instrumentation configurations.
| Run | Counters present | What it establishes |
|---|---|---|
| A | the RMON six | class G — five candidates |
| B | + c_symbol_errors | still five — the wrong counter |
| C | + c_filtered | four |
| D | + c_vlan_discards, c_fifo_drops | two |
| E | + c_desc_errors | one — the site |
The oracle is four-part and every part is checkable without knowing the answer.
| Part | A | B | C | D | E |
|---|---|---|---|---|---|
| candidates at termination | 5 | 5 | 4 | 2 | 1 |
best_possible_answer at step 0 | 5 | 5 | 4 | 2 | 1 |
method_sound | true | true | true | true | true |
| observations made | 3 | 4 | 5 | 7 | 7 |
Row three is the control and it is why the test is conclusive. The procedure was sound in all five runs — no wasted steps, no premature capture, no conclusion from an unproven zero. Row one moved from five to one and row three never changed, so the improvement is entirely instrumentation and none of it is method.
Row two is the test's real product. best_possible_answer predicted row one in every run, at step 0, from a list of which registers exist — before a single frame was sent. A procedure that can tell you its own ceiling before it starts is a procedure that can be planned around, and Section 15's whole second output group exists for that one row.
Run B is the instructive failure. Adding a counter improved the class count from seven to eight and improved the answer by nothing, because c_symbol_errors separates class B and the fault was in class G. More instrumentation is not better instrumentation, and the four counters that matter are named in Section 4.
22. Debugging the Debugger
Six complaints about the method itself.
Complaint 1 — "we have been measuring for an hour and the list is not getting shorter."
| Check | If yes | Meaning |
|---|---|---|
no_useful_question? | yes | the procedure finished — Section 9 |
n_candidates = 5? | yes | class G |
| are the four optional counters present? | no | the answer, and it was knowable at step 0 |
wasted_steps climbing? | yes | the same questions are being re-asked |
Row three is the whole diagnosis and it needed a datasheet rather than an hour. best_possible_answer reports it before the first observation, and a team that reads it first spends the hour on the four candidates it cannot separate rather than on measurements that cannot separate them.
Complaint 2 — "we eliminated the FIFO and the fault was in the FIFO."
| Check | If yes | Meaning |
|---|---|---|
was c_fifo_drops read as zero? | yes | the elimination came from that |
| was it proven present? | no | Section 10 — the trap |
| does the register exist in the datasheet? | no | it read zero because it is not there |
probe_skipped set? | yes, throughout | the monitor said so and nobody looked |
This is the most expensive error in the chapter and it is one register. An unimplemented counter reads zero exactly like an implemented one at rest, and the site it would have exonerated is the site the fault was at. Section 11's probe takes four thousand frames and settles it permanently for the platform.
Complaint 3 — "the candidate list went empty."
| Check | If yes | Meaning |
|---|---|---|
contradiction set? | yes | two observations are inconsistent |
| which two? | the log — Section 13 | before_mask per step |
| is one of them from an absent counter? | often | an absence treated as a zero |
| if not | — | Section 2's table is wrong for this design |
Row four is rare and is the one worth chasing. A genuinely inconsistent pair of readings means a counter moves for a site the table says it does not — a real fact about this MAC that nobody had written down. Fixing the table is worth more than fixing the outage, because the table is used by every future investigation.
Complaint 4 — "the fix did not work."
| Check | If yes | Meaning |
|---|---|---|
| did the symptom change at all? | no | wrong site, or a class reported as a site |
| was the terminal state a class? | yes | a one-in-five guess — Section 14's row four |
is unaccounted still non-zero? | yes | Section 17 — a second fault |
| did the mask converge in under 7 steps? | yes | lucky; check the log |
Row three is the check nobody runs and it costs six register reads. Two simultaneous faults converge the mask on the earlier one, the fix is correct, the symptom persists, and the method looks discredited — when the conservation identity would have said, in one step, that loss remained.
Complaint 5 — "the counters say zero but the link is clearly broken."
| Check | If yes | Meaning |
|---|---|---|
| what is the line rate? | 1 Gb/s | Section 18's row nine |
| how long was the window? | one second | 1.488 M frames |
| what fault rate would that miss? | 1 in 10⁷ | one event every 6.7 s |
| retry with a 60-second window | about 9 events | detected |
The same fault at 100 Gb/s appears fifteen times in one second and at 1 Gb/s appears once in seven. A zero is only evidence if the window was long enough, and the required window scales inversely with the line rate — which is the opposite of the instinct that slow links are easier to debug.
Complaint 6 — "the counter reads exactly half of what the capture shows."
| Check | If yes | Meaning |
|---|---|---|
| is the traffic runts? | yes | Chapter 19.7 §7 |
| are they 16 to 47 octets? | yes | two frames can end in one beat |
| is the counter's addend one bit? | yes | it counts one of every two |
| is the ratio clean? | exactly 0.5 | the most identifiable counter bug there is |
A counter wrong by a clean fraction is a design bug and not a measurement error, and it is the one case in this chapter where the instrumentation itself is the fault. Chapter 19.7 §7 derived the threshold at 48 octets — below it a 64-octet beat holds two frame endings — and a design that sized its addend for conformant traffic counts exactly half of a 47-octet flood and exactly a third of a 15-octet one.
Complaint 7 — "the same procedure gave two different answers on two ports."
| Check | If yes | Meaning |
|---|---|---|
same optional_present on both? | no | different ceilings, so different answers |
| same line rate? | no | Section 18's row nine — different window sensitivity |
same skew_bound? | no | the conservation identity has a different tolerance |
| same MAC design? | no | Section 2's table may not apply to both |
All four rows can be no on two ports of the same switch, which is the awkward part. A 1 Gb/s access port and a 100 Gb/s uplink differ in every one of them: the uplink has richer statistics, a hundred times the window sensitivity, and a twelve-frame skew where the access port has 0.19. The procedure is identical and the evidence it collects is not comparable, which is Chapter 20.6 §8's problem — a constant that is secretly the line rate — arriving in an operations team rather than in a testbench.
And the three symptoms this chapter is systematically blamed for:
| Symptom | Blamed on | Usually is |
|---|---|---|
| "the method did not find it" | the method | class G — Section 4 |
| "the counters lied" | the design | an absent counter read as a zero |
| "we fixed it and it came back" | the diagnosis | a second fault — Section 17 |
23. Misconceptions
Misconception 1 — "CRC errors mean a bad cable."
The wrong model: the check sequence protects the frame across the channel, so a failing check sequence means the channel damaged the frame.
What it costs: c_crc_errors moves for five of the twelve sites — the channel, the PHY's lane alignment, the xMII interface, the parser's offset arithmetic and the check engine's own logic. Four of those five are inside the chip. Replacing a cable is right about one time in five, and the four wrong times each consume a maintenance window and leave the fault in place.
The corrected model: a moving CRC counter is the best single question available and it is worth 0.98 bits — it narrows twelve to five. The next two register reads narrow five to one, and they cost a millisecond each. Section 6.
Misconception 2 — "a counter reading zero rules that site out."
The wrong model: the counter for site X reads zero, therefore the fault is not at site X.
What it costs: it depends on something the reading cannot tell you — whether the counter exists. An unimplemented statistics register very often reads zero rather than faulting, so "zero" and "not there" are byte-identical on the bus — and the four counters most often absent are the four that observe class G, which is the five sites the method most needs to separate.
The corrected model: a zero eliminates the counter's sites; an absence eliminates nothing. Prove the difference once per platform by making each counter move on purpose — a misaddressed frame for c_filtered, a non-member VLAN tag for c_vlan_discards — and record the result. Sections 10 and 11.
Misconception 3 — "take a capture first, then you can see what is happening."
The wrong model: the frames are the ground truth, so start by looking at them.
What it costs: three hours of setup, a span port or a tap, a storage decision, and an observation worth about one bit — because a capture at one point in a topology tells you frames were or were not there, which is frames_in and frames_out with more steps. Meanwhile five register reads would have taken twelve candidates to one or five.
The corrected model: the capture is step 6, and by then it is aimed: one interface, one condition, one oracle. Section 12's first five steps are register reads and cost milliseconds, and the whole procedure up to the capture can be run from a script.
Misconception 4 — "five candidates means the investigation failed."
The wrong model: a diagnosis that does not name a component has not finished.
What it costs: it pushes an investigator into choosing one of the five — by prior, by habit, by which team is easiest to talk to — and reporting a one-in-five guess as a conclusion. When it is wrong, and it is wrong four times in five, the method is discredited rather than the guess.
The corrected model: "the filter, VLAN, the FIFO, the DMA or the driver, and c_fifo_drops would separate them" is a finished diagnosis. It names the next action correctly — instrumentation, not investigation — and Section 15 could have predicted it at step 0.
Misconception 5 — "more counters would help."
The wrong model: the fault space is under-observed, so add observability.
What it costs: it is true and unhelpfully vague, and the obvious addition is the wrong one. Adding c_symbol_errors raises the class count from seven to eight and leaves the largest class at five, because it splits class B and class B was never the problem. Four specific counters matter, they are the four that observe non-errors, and each buys exactly one site.
The corrected model: the four are c_filtered, c_vlan_discards, c_fifo_drops and c_desc_errors, about 297 flops — 2.1% of the datapath. And the change that would matter more is different in kind: twelve boundary counters make the search a bisection and take it from seven observations to four, for 8.4%. Section 8.
Misconception 6 — "we found the fault, so the method worked."
The wrong model: a correct outcome validates the procedure.
What it costs: the two are independent. A procedure can be 40% efficient, take ten observations, and converge by luck; it can be perfectly efficient and finish with five candidates because of the platform. Judging the method by the outcome means learning nothing from either case — and the efficient-but-limited case is the one that gets a team to change the wrong thing.
The corrected model: report two numbers. efficiency_pct is about the procedure; n_candidates against best_possible_answer is about the platform. A run at 100% efficiency that ends with five candidates is a perfect investigation of an under-instrumented port, and the action it implies is a design change.
24. Interview Questions
Question 1 — "A link is dropping frames. What is the first thing you look at?"
What the answer should establish: that frames_out is the trigger and not a measurement. It falls short for every possible fault, so it eliminates nothing — zero bits. A strong answer names the real first step: the conservation identity, which balances or does not and cuts twelve candidates to eight or four for the cost of six register reads. The strongest answer asks a question first: which optional counters does this platform implement, because the answer sets the ceiling on everything that follows.
Question 2 — "CRC errors are climbing. Is the cable bad?"
What the answer should establish: that five sites move that counter — the channel, the PHY's lane alignment, the xMII, the parser's offset arithmetic and the check engine itself — and four of them are inside the chip. A strong answer gives the next step: c_alignment_errors separates the physical three from the logical two, and frames_in separates the sites before the port counter from those after. Two more register reads, and the five become one.
Question 3 — "Frames go in, fewer come out, and no error counter moves. What is wrong?"
What the answer should establish: that this is a class and not a fault — the address filter, VLAN membership, the receive FIFO, the DMA path or the host driver, five sites, all of which discard frames legally and none of which is an error. A strong answer explains why the standard is silent: RMON counts errors, and a legal discard is not one. The strongest answer names which counter separates which site and notes that all four are optional.
Question 4 — "Twelve candidates is under four bits. Why does a diagnosis take seven steps?"
What the answer should establish: that the counters do not partition the space evenly. The best available question, c_crc_errors, splits five from seven — 0.98 bits — and the four decisive counters at the end split one from eleven, which is 0.414 bits each and cannot be improved by ordering. A strong answer draws the conclusion: the tail of the search is linear, so the cost is 4 + (number of one-site indicators needed).
Question 5 — "Your counter reads zero. How do you know it works?"
What the answer should establish: make it move on purpose. A thousand frames to an address the port does not own must increment c_filtered; a tagged frame for a VLAN the port is not in must increment c_vlan_discards. If the counter stays at zero, it is not implemented — and that is now a fact rather than an assumption. A strong answer notes the exception: c_fifo_drops cannot be provoked from the wire, because no sequence of frames overflows a FIFO whose drain rate exceeds the line rate.
Question 6 — "You would add one thing to a MAC to make it debuggable. What?"
What the answer should establish: not another error counter. A plain frame counter at each of the twelve boundaries — entering the parser, the filter, the FIFO, the DMA — turns the search from a lopsided decision tree into a bisection, four observations instead of seven. A strong answer costs it: about 1 188 flops against a 14 166-flop datapath, 8.4%. The strongest answer says why it does not get built: the cost falls on the design team and the benefit falls on whoever is paged at three in the morning.
25. Questions and Answers
26. What's Next
This chapter built the method. The rest of Module 21 is what the method is applied to.
| Chapter | Supplies |
|---|---|
| Chapter 21.2 | the symptom catalogue — what each error class physically means |
| Chapter 21.3 | the first class, and the channel-versus-logic separation |
| Chapter 21.4 | a link that never reaches a usable state — a different fault space |
| Chapter 21.9 | step 6, and what a capture cannot show |
Chapter 21.2 fills in Section 2's table from the other direction. This chapter took the counters as given and asked what they separate; the taxonomy asks what each error class physically is — Chapter 7.3's five widened to symbol errors, drops, overflows and descriptor faults. The two together are the method: a table of sites, a table of symptoms, and an intersection.
Chapter 21.3 is the first entry and it is the one this chapter kept returning to. c_crc_errors is the best single question in the method — 0.98 bits, five sites of twelve — and four of those five are inside the chip. Separating the channel from the logic is Chapter 19.4 §14's equivalence checker on one side and a marginal connector on the other, and the distinction is worth a chapter because the maintenance action is completely different.
And the series is now ninety-four classes long. Chapter 20.2 §8's six-group taxonomy has already needed extending twice — Chapter 20.4's class 91 opened a seventh group and Chapter 20.6's class 93 fits none of them. Class 94 is a third kind again: a property about a search whose subject is really the sensors. What the last three have in common is that the property is correct about the thing it names and wrong about the thing it constrains — which may be the eighth group, and Chapter 21.2 is where a taxonomy of errors gets argued about properly.
Continue learning
Related tutorials
- Related topic
The Ethernet Error Taxonomy
Seven of the twelve reachable frame shapes belong to more than one error class, so exclusivity comes from a priority rule — and the two obvious rules disagree on exactly half the space.
- Related topic
Packet Drops
Five sites, one signature, and two ratios over counters that already exist take them to three classes — plus the finding that two of the four optional counters are nearly redundant.
- Related topic
Statistics Counters
Twenty-seven counters, four of them 64 bits because a 32-bit octet counter wraps every 0.344 seconds, and a shadow bank that doubles the block because two counters must be read together.
- Related topic
CRC Errors
A channel fault's error rate is proportional to frame length and every logic fault's is flat, so the ratio is 23.72 or 1.00 — measurable on counters a MAC already computes.
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.
