Skip to content
VLSI Mentor

Ethernet · Module 11

Negotiation Failures and Duplex Mismatch

Seven ways a link comes up wrong and six of them report no error, because every device behaved correctly. Diagnosis is set narrowing over evidence, and three causes cannot be seen from one end at all.

Three chapters have each contributed a way for a link to come up and be wrong.

Chapter 11.1's lost pulse removes an advertised ability, so the priority resolver cleanly selects the next option down — both ends agreeing, no counter moving. Chapter 11.2's table disagreement has two devices resolve differently against a table that is never transmitted. Chapter 11.3's skipped stage 4 leaves duplex assumed rather than agreed.

And Chapter 9.2 §7 gave the mechanism that turns the last one into an outage weeks later.

This chapter owns the taxonomy and the diagnosis, and the diagnosis is harder than the mechanisms, for one structural reason.

The mapping from symptom to cause is many-to-many. One hundred per cent FCS failures is produced by a duplex mismatch, an RGMII delay error, a swapped XAUI lane, a wrapped deskew FIFO, a nibble swap and a dead cable. A symptom does not imply a cause; it narrows a set — and a design that reports one cause has encoded a heuristic as a fact.

1. Scope — What This Chapter Owns

This chapter owns the taxonomy and the decision procedure: which causes exist, which symptoms each produces, which observations discriminate, and what to do when no single end has enough evidence.

It does not re-derive the mechanisms. Chapter 9.2 §7–§10 owns the duplex-mismatch mechanism and its asymmetric signature; Chapter 11.1 §6 owns the lost-pulse downgrade; Chapter 11.2 §6 owns the table disagreement; Chapter 11.3 §7 owns the skipped-stage path and §6 the stale-measurement failure.

Module 11 ends here. Chapter 12.1 opens the module that owns switching.

The claim this chapter defends: a symptom narrows a set of causes rather than implying one, so a diagnostic design must publish the consistent set and the evidence still missing to discriminate — and must never report a single cause it has not earned.

Seven distinct causes make an Ethernet link come up in a wrong configuration. A duplex mismatch, in which one end negotiated full duplex and the other assumed half. A lost advertisement pulse, which removes an ability and negotiates one step down. A priority table disagreement, in which both ends resolve correctly against different tables. Parallel detection applied to a partner that merely negotiates slowly. An over advertisement of an ability the hardware cannot deliver. A stage of bring up run against a measurement from a previous attempt. And a one sided forced configuration, where one end is administratively fixed while the other negotiates. Each produces a set of symptoms, and almost every symptom is produced by several of them.Duplex mismatchone assumed, one agreedLost pulsenegotiates one step downTable disagreementboth resolved correctlySlow negotiatordetected10 Mb/s between gigabitportsOver-advertisementwins, then cannotestablishStale measurementold cable's coefficientsOne end forcedthe other detects12
Figure 1 — seven causes, and the observable that separates each from the ones it shares symptoms with.
#CauseWhere it is establishedReports an error?
1duplex mismatch9.2 §7no — both ends healthy until load
2lost advertisement pulse11.1 §6no — negotiates cleanly, one step down
3priority table disagreement11.2 §6no — both resolved correctly
4slow negotiator parallel-detected11.1 §8no — detection succeeded
5over-advertisement11.2 §4partly — the link fails to establish
6stale measurement reused11.3 §6no — every stage succeeded
7one end administratively forced9.2 §9no — the forced end sends no bursts

Six of the seven report no error at all, and the seventh reports one that looks like a cabling fault.

Which is not an accident of implementation. Every one of these is a case in which each device behaved correctly against the information it had. The lost pulse produced a smaller advertisement, and the resolver correctly took the highest common member of a smaller set. The table disagreement had two correct resolutions against two tables. The forced end correctly declines to negotiate.

There is no wrong actor anywhere, which is precisely why no counter fires — counters count things that went wrong, and nothing did.

3. The Symptom Matrix

Write out which causes produce which symptoms, because the shape of the table is the chapter's argument.

Symptom1 duplex2 lost pulse3 table4 slow neg.5 over-adv.6 stale7 forced
speed below expectation
duplex below expectation
FCS errors under load
collisions at one end only
late collisions
link never establishes
renegotiation storm
zero errors, low throughput

Count the columns each row touches.

SymptomCauses it is consistent withInformation it gives
collisions at one end only2narrows 7 → 2 — 1.81 bits
late collisions21.81 bits
link never establishes12.81 bits — the only decisive one
renegotiation storm21.81 bits
FCS errors under load31.22 bits
speed below expectation40.81 bits
duplex below expectation50.49 bits
zero errors, low throughput50.49 bits

The two most-reported symptoms are the two least informative. Speed below expectation and zero errors, low throughput — the two things a user actually notices — each leave four or five causes standing.

And the two most decisive symptoms both require the far end. Collisions at one end only is, by construction, a statement about two ends: on a genuinely shared segment both ends collide. Late collisions are decisive because Chapter 9.1 §5 established that they cannot arise from contention on a conforming segment — but they are only decisive when paired with the other end's collision count being zero.

4. RTL 1 — Classifying a Symptom Into a Set

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// SYNTHESIZABLE.
//
// Observes symptoms and maintains the SET of causes still consistent
// with everything seen so far.
//
// WHY A SET AND NOT A VERDICT. Section 3's matrix is many-to-many:
//   "speed below expectation"      is consistent with 4 causes
//   "duplex below expectation"     with 5
//   "zero errors, low throughput"  with 5
//   "collisions at one end only"   with 2   <- the decisive one
//   "link never establishes"       with 1   <- the only decisive one
//
// So a classifier that emits one cause has encoded a HEURISTIC as a
// FACT. It will be right most of the time, which is exactly what makes
// it dangerous: it reports the common cause confidently, and the
// uncommon one identically.
//
// THIS MODULE instead maintains a bitmask of consistent causes, starts
// with all of them, and clears bits only when an observation is
// INCONSISTENT with a cause. Which means it can also report the one
// thing a verdict cannot: WHAT EVIDENCE IS STILL MISSING.
package diag_pkg;
 
  localparam int unsigned N_CAUSES = 7;
 
  // Cause bit positions.
  localparam int unsigned C_DUPLEX_MISMATCH = 0;
  localparam int unsigned C_LOST_PULSE      = 1;
  localparam int unsigned C_TABLE_DISAGREE  = 2;
  localparam int unsigned C_SLOW_NEGOTIATOR = 3;
  localparam int unsigned C_OVER_ADVERTISE  = 4;
  localparam int unsigned C_STALE_MEASURE   = 5;
  localparam int unsigned C_ONE_END_FORCED  = 6;
 
  // Symptom bit positions.
  localparam int unsigned S_SPEED_LOW       = 0;
  localparam int unsigned S_DUPLEX_LOW      = 1;
  localparam int unsigned S_FCS_UNDER_LOAD  = 2;
  localparam int unsigned S_ONE_SIDED_COLL  = 3;
  localparam int unsigned S_LATE_COLLISION  = 4;
  localparam int unsigned S_NO_ESTABLISH    = 5;
  localparam int unsigned S_RENEG_STORM     = 6;
  localparam int unsigned S_LOW_THROUGHPUT  = 7;
 
  // Section 3's matrix, as a per-symptom mask of CONSISTENT causes.
  // Observing a symptom intersects the live set with its mask.
  function automatic logic [N_CAUSES-1:0] consistent_with (input int sym);
    unique case (sym)
      S_SPEED_LOW:      consistent_with = 7'b1001110;
      S_DUPLEX_LOW:     consistent_with = 7'b1001111;
      S_FCS_UNDER_LOAD: consistent_with = 7'b1100001;
      S_ONE_SIDED_COLL: consistent_with = 7'b1000001;
      S_LATE_COLLISION: consistent_with = 7'b1000001;
      S_NO_ESTABLISH:   consistent_with = 7'b0010000;
      S_RENEG_STORM:    consistent_with = 7'b0010010;
      S_LOW_THROUGHPUT: consistent_with = 7'b1001111;
      default:          consistent_with = 7'b1111111;
    endcase
  endfunction
 
endpackage
 
module symptom_classifier
  import diag_pkg::*;
#(
  parameter int unsigned CNT_W = 16
) (
  input  logic clk,
  input  logic rst_n,
  input  logic clear,
 
  input  logic [7:0] symptom_seen,        // one bit per symptom, level
  input  logic       symptom_valid,
 
  output logic [N_CAUSES-1:0] consistent_causes,
  output logic [3:0]          consistent_count,
  output logic [7:0]          symptoms_observed,
 
  // A single cause remains. Reported as a DIAGNOSIS only here -- never
  // when the set has more than one member.
  output logic                diagnosis_reached,
  output logic [2:0]          diagnosis_cause,
 
  // No cause is consistent with everything observed. Which means the
  // taxonomy is incomplete, or two faults are present at once, or an
  // observation is wrong -- and all three are worth knowing.
  output logic                inconsistent_evidence,
 
  output logic [CNT_W-1:0]    c_narrowings,
  output logic [CNT_W-1:0]    c_diagnoses,
  output logic                ever_inconsistent
);
 
  logic [N_CAUSES-1:0] live_q;
  logic [7:0]          seen_q;
  logic [N_CAUSES-1:0] next_c;
  logic [3:0]          count_c;
 
  assign consistent_causes = live_q;
  assign symptoms_observed = seen_q;
 
  always_comb begin
    next_c = live_q;
    for (int s = 0; s < 8; s = s + 1)
      if (symptom_seen[s]) next_c = next_c & consistent_with(s);
 
    count_c = 4'd0;
    for (int c = 0; c < N_CAUSES; c = c + 1)
      if (next_c[c]) count_c = count_c + 4'd1;
  end
 
  always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n || clear) begin
      // START WITH EVERYTHING. A diagnostic that starts from a
      // suspicion has already narrowed on no evidence.
      live_q <= {N_CAUSES{1'b1}};
      seen_q <= 8'd0;
      consistent_count <= 4'(N_CAUSES);
      diagnosis_reached <= 1'b0; diagnosis_cause <= 3'd0;
      inconsistent_evidence <= 1'b0;
      if (!rst_n) begin
        c_narrowings <= '0; c_diagnoses <= '0; ever_inconsistent <= 1'b0;
      end
    end else if (symptom_valid) begin
      seen_q <= seen_q | symptom_seen;
 
      // MONOTONE NARROWING. The set only ever shrinks; an observation
      // never re-admits a cause it previously excluded, because
      // evidence does not un-happen.
      if (next_c != live_q) begin
        live_q <= next_c;
        if (!(&c_narrowings)) c_narrowings <= c_narrowings + 1'b1;
      end
      consistent_count <= count_c;
 
      // A DIAGNOSIS is one cause and only one cause.
      diagnosis_reached <= (count_c == 4'd1);
      if (count_c == 4'd1) begin
        for (int c = 0; c < N_CAUSES; c = c + 1)
          if (next_c[c]) diagnosis_cause <= 3'(c);
        if (!(&c_diagnoses)) c_diagnoses <= c_diagnoses + 1'b1;
      end
 
      // EMPTY SET. Not "no fault" -- the opposite. Either the taxonomy
      // is incomplete, two faults are present, or an observation is
      // wrong, and reporting it as "unknown" would hide all three.
      inconsistent_evidence <= (count_c == 4'd0);
      if (count_c == 4'd0) ever_inconsistent <= 1'b1;
    end
  end
 
endmodule

Classification: synthesizable.

What it teaches: that an empty consistent set is a finding rather than a failure to find one. If no cause explains every observation, then the taxonomy is incomplete, or two faults are present simultaneously, or one of the observations is wrong — and all three are worth knowing. A classifier that reports "unknown" when it means "contradictory" has merged the most and least informative outcomes it can produce.

Deliberately simplified: eight symptoms and seven causes with a static matrix. Production diagnostics carry likelihoods rather than a boolean mask, so a rare cause consistent with the evidence can be ranked below a common one without being eliminated.

Production implication: the narrowing is monotone — the set only shrinks — because evidence does not un-happen. A design that re-admits a cause when a symptom stops being observed will oscillate: a duplex mismatch produces FCS errors only under load, so the symptom disappears at night and the diagnosis with it. symptoms_observed accumulates rather than sampling, for exactly that reason.

5. Why Most Symptoms Are Shared

The many-to-many mapping is not an accident of this taxonomy. It follows from where the causes sit.

All seven causes act before any frame exists. They corrupt a configuration — a speed, a duplex, a set of coefficients — and a configuration error expresses itself only through the frames that later cross the misconfigured link.

So every one of them is observed through the same narrow aperture: the frame layer. And the frame layer has a small vocabulary — frames arrived or did not, passed FCS or did not, collided or did not — which cannot possibly distinguish seven upstream causes.

LayerVocabulary
the causesspeed, duplex, abilities, tables, coefficients, epochs
the frame layerdelivered, corrupt, collided, absent

Four observable outcomes for seven causes. The mapping cannot be one-to-one, and no amount of care at the frame layer changes that.

Which points at where the discriminating evidence has to come from, and it is the same answer this track has reached repeatedly: below the combine.

Chapter 9.3 needed per-pair margin because 8B1Q4 destroys pair identity. Chapter 9.5 needed per-lane BIP because the re-interleaver destroys lane identity. Chapter 11.1 needed per-bit confidence because the decoded word destroys the distinction between a zero and a loss.

Here the combining step is the link itself, and what it destroys is provenance — how each configuration value was arrived at. A speed that was measured and a speed that was assumed produce identical frames.

6. RTL 2 — Correlating Two Ends

A duplex mismatch produces collisions and late collisions at the half duplex end and frame check sequence errors and runt frames at the full duplex end. Each end alone reports something that resembles a physical fault: the half duplex end looks like a congested segment and the full duplex end looks like a bad cable. Only placing the two side by side shows the pattern that identifies the cause, which is collisions at one end and none at the other, since a genuinely shared segment collides at both. A priority table disagreement is even more extreme, because neither end reports anything wrong at all and the disagreement is visible only by comparing the two resolved technologies.Half-duplex endcollisions, latecollisionsFull-duplex endFCS errors, runtsEach alonelooks like cablingThe paircollisions at ONE endTable disagreementneither reports anythingCompare resolutionsthe only observation12
Figure 2 — three causes have symmetric-looking symptoms at each end and are identified only by the pair.
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// SYNTHESIZABLE.
//
// Correlates evidence from BOTH ends of a link, because three of the
// seven causes cannot be identified from one end at all.
//
// THE THREE:
//   DUPLEX MISMATCH  -- the evidence is ASYMMETRIC. The half-duplex end
//        sees collisions and late collisions; the full-duplex end sees
//        FCS errors and runts. Each alone tells a plausible and wrong
//        physical-layer story: "the segment is congested" and "the
//        cable is bad". The pair is unmistakable, because a genuinely
//        shared segment collides at BOTH ends.
//   TABLE DISAGREEMENT -- neither end reports anything wrong. Both
//        resolved correctly against their own table. The ONLY
//        observation is that the two resolved technologies differ.
//   ONE END FORCED   -- the forced end sends no FLP bursts and reports
//        a clean forced configuration; the other parallel-detects.
//        Each is internally consistent.
//
// This module takes a remote evidence record -- delivered over
// management, LLDP, or an operator typing two register dumps into one
// tool -- and computes the comparisons no single end can.
module two_ended_evidence_correlator
  import diag_pkg::*;
#(
  parameter int unsigned CNT_W = 16
) (
  input  logic clk,
  input  logic rst_n,
  input  logic clear,
 
  // Local evidence.
  input  logic [15:0] local_collisions,
  input  logic [15:0] local_late_collisions,
  input  logic [15:0] local_fcs_errors,
  input  logic [15:0] local_runts,
  input  logic [3:0]  local_resolved_tech,
  input  logic        local_full_duplex,
  input  logic        local_forced,
  input  logic        local_duplex_assumed,
 
  // Remote evidence, and a validity bit, because it arrives by a
  // completely different path and may be absent, stale or wrong.
  input  logic        remote_valid,
  input  logic [15:0] remote_collisions,
  input  logic [15:0] remote_late_collisions,
  input  logic [15:0] remote_fcs_errors,
  input  logic [15:0] remote_runts,
  input  logic [3:0]  remote_resolved_tech,
  input  logic        remote_full_duplex,
  input  logic        remote_forced,
 
  // The comparisons.
  output logic asymmetric_collisions,   // one end collides, the other does not
  output logic asymmetric_errors,       // one end FCS-errs, the other does not
  output logic resolution_disagreement, // the two ends chose differently
  output logic duplex_disagreement,
  output logic both_forced_differently,
 
  // The verdicts these comparisons support, as a cause mask.
  output logic [N_CAUSES-1:0] causes_supported,
  output logic [N_CAUSES-1:0] causes_excluded,
 
  // Correlation was attempted without remote evidence. Reported,
  // because "we could not tell" and "there is nothing wrong" are
  // different answers and a design must not conflate them.
  output logic remote_evidence_missing,
  output logic [CNT_W-1:0] c_correlations,
  output logic [CNT_W-1:0] c_missing_remote,
  output logic             ever_disagreed
);
 
  // "One end and not the other" needs a threshold, because zero is
  // rarely exactly zero on a real link.
  localparam logic [15:0] NOISE_FLOOR = 16'd8;
 
  wire local_colls_high  = (local_collisions  > NOISE_FLOOR);
  wire remote_colls_high = (remote_collisions > NOISE_FLOOR);
  wire local_fcs_high    = (local_fcs_errors  > NOISE_FLOOR);
  wire remote_fcs_high   = (remote_fcs_errors > NOISE_FLOOR);
 
  always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n || clear) begin
      asymmetric_collisions <= 1'b0; asymmetric_errors <= 1'b0;
      resolution_disagreement <= 1'b0; duplex_disagreement <= 1'b0;
      both_forced_differently <= 1'b0;
      causes_supported <= '0; causes_excluded <= '0;
      remote_evidence_missing <= 1'b0;
      if (!rst_n) begin
        c_correlations <= '0; c_missing_remote <= '0; ever_disagreed <= 1'b0;
      end
    end else begin
      if (!remote_valid) begin
        // NO REMOTE EVIDENCE. Not "no disagreement" -- unknown. Three
        // causes remain unresolvable, and saying so is the honest
        // output.
        remote_evidence_missing <= 1'b1;
        causes_supported        <= '0;
        causes_excluded         <= '0;
        if (!(&c_missing_remote)) c_missing_remote <= c_missing_remote + 1'b1;
 
      end else begin
        remote_evidence_missing <= 1'b0;
        if (!(&c_correlations)) c_correlations <= c_correlations + 1'b1;
 
        // THE DECISIVE COMPARISON. On a genuinely shared segment BOTH
        // ends collide. Collisions at one end and not the other means
        // one end is not arbitrating at all.
        asymmetric_collisions <= (local_colls_high != remote_colls_high);
 
        // And its counterpart: the end that does not collide sees the
        // other's jam as FCS errors and runts.
        asymmetric_errors <= (local_fcs_high != remote_fcs_high);
 
        // NEITHER END REPORTS ANYTHING for this one. The comparison is
        // the entire observation.
        resolution_disagreement <= (local_resolved_tech != remote_resolved_tech);
        duplex_disagreement     <= (local_full_duplex   != remote_full_duplex);
        both_forced_differently <= local_forced && remote_forced &&
                                   (local_full_duplex != remote_full_duplex);
 
        causes_supported <= '0;
        causes_excluded  <= '0;
 
        if ((local_colls_high != remote_colls_high) &&
            (local_fcs_high   != remote_fcs_high)) begin
          // Asymmetric in BOTH directions: the duplex-mismatch
          // signature, and it excludes every cause that is symmetric.
          causes_supported[C_DUPLEX_MISMATCH] <= 1'b1;
          causes_supported[C_ONE_END_FORCED]  <= 1'b1;
          causes_excluded[C_LOST_PULSE]       <= 1'b1;
          causes_excluded[C_SLOW_NEGOTIATOR]  <= 1'b1;
          ever_disagreed <= 1'b1;
        end
 
        if (local_resolved_tech != remote_resolved_tech) begin
          // Two correct resolutions that differ. Chapter 11.2 §6's
          // table disagreement, and nothing else produces it.
          causes_supported[C_TABLE_DISAGREE] <= 1'b1;
          ever_disagreed <= 1'b1;
        end
 
        if (local_colls_high && remote_colls_high) begin
          // BOTH ends collide: a genuinely shared half-duplex segment.
          // Which EXCLUDES a duplex mismatch, and that exclusion is as
          // valuable as any positive finding.
          causes_excluded[C_DUPLEX_MISMATCH] <= 1'b1;
          causes_excluded[C_ONE_END_FORCED]  <= 1'b1;
        end
      end
    end
  end
 
endmodule

Classification: synthesizable.

What it teaches: that remote_evidence_missing is a distinct output from "no disagreement", and merging them is the most common mistake a correlator makes. Without the far end's counters, three of the seven causes are simply unresolvable — and a design that reports "no disagreement found" when it means "we could not look" tells an operator the link is fine. "We could not tell" and "there is nothing wrong" are different answers.

Deliberately simplified: remote evidence arrives as a flat record. In practice it comes over LLDP, over a management plane, or from an operator pasting two register dumps into one tool — and each path has its own staleness problem.

Production implication: causes_excluded is as valuable as causes_supported and is usually the neglected half. Both ends colliding means a genuinely shared segment, which excludes a duplex mismatch — and eliminating the most-suspected cause is often what unblocks an investigation. A correlator that only reports positive findings leaves an operator narrowing by suspicion instead of by evidence.

7. What Needs Both Ends

Be precise about which causes are and are not resolvable from one end, because it decides what a single device can honestly claim.

CauseResolvable from one end?The observation that resolves it
1 duplex mismatchnocollisions at one end and not the other
2 lost pulseyescommon_count against the expected advertisement
3 table disagreementnothe two ends' resolved technologies differ
4 slow negotiator detectedyesever_parallel_detected, c_aborts
5 over-advertisementyesever_over_advertised, refused_bits
6 stale measurementyesstale_measurement_used
7 one end forcednothe far end reports a forced configuration

Four of seven are resolvable locally, and every one of the four is resolvable from provenance rather than from errors.

Three are not, and they share a structure: each is a disagreement between two correct devices. There is no local anomaly to observe because there is no local anomaly — the local device did exactly the right thing with the information it had.

Which produces a rule worth stating plainly: a single device can diagnose its own weak assumptions and cannot diagnose a disagreement. The first is introspection; the second requires a second observer.

And the practical consequence is about tooling rather than silicon. The four local causes are register reads on one device. The three remote ones need two devices' registers compared — which is a management-plane capability, and its absence is why duplex mismatches were an industry-wide problem for a decade rather than a five-minute check.

8. RTL 3 — Predicting From Provenance

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// SYNTHESIZABLE.
//
// Predicts a mismatch BEFORE the symptom, from provenance rather than
// from errors.
//
// THE OBSERVATION THIS RESTS ON. A duplex mismatch needs SIMULTANEOUS
// transmission to produce any symptom at all. At low utilisation that
// almost never happens:
//
//   1% each way  -> P(simultaneous) ~ 0.0001 -> 1 event in 10 000 slots
//   5% each way  -> 0.0025                   -> 1 in 400
//   20% each way -> 0.04                     -> 1 in 25
//   50% each way -> 0.25                     -> 1 in 4
//
// So the link tests clean at commissioning, passes acceptance, and
// degrades in production -- weeks later, under load, with error
// counters that point at the cable.
//
// AND THE PREDICTIVE EVIDENCE WAS AVAILABLE AT BRING-UP. Four of the
// seven causes leave a provenance mark in the milliseconds after link
// up, on a link that is working perfectly:
//   duplex_is_assumed        -- Chapter 11.1 §8
//   ever_parallel_detected   -- Chapter 11.1 §8, sticky
//   common_count == 1        -- Chapter 11.2 §12, no fallback
//   ever_over_advertised     -- Chapter 11.2 §4
//   stale_measurement_used   -- Chapter 11.3 §6
//
// None of them is an error. All of them predict.
module mismatch_predictor
  import diag_pkg::*;
#(
  parameter int unsigned CNT_W = 16
) (
  input  logic clk,
  input  logic rst_n,
  input  logic clear,
 
  input  logic link_came_up,
 
  // Provenance, carried forward from Chapters 11.1 through 11.3.
  input  logic duplex_is_assumed,
  input  logic ever_parallel_detected,
  input  logic [3:0] common_count,
  input  logic ever_over_advertised,
  input  logic stale_measurement_used,
  input  logic detection_used,
  input  logic near_timeout,
  input  logic [3:0] resolved_tech,
 
  // Utilisation, so the prediction can say WHEN it will surface.
  input  logic [6:0] tx_utilisation_percent,
  input  logic [6:0] rx_utilisation_percent,
 
  // The prediction: which causes this link is EXPOSED to, before any
  // symptom has appeared.
  output logic [N_CAUSES-1:0] exposed_causes,
  output logic [2:0]          exposure_count,
  output logic                link_is_exposed,
 
  // Risk, expressed as the two things that decide it: how weak the
  // provenance is, and how much load will exercise the weakness.
  output logic [1:0]  provenance_strength,   // 3 strong .. 0 assumed
  output logic [13:0] simultaneity_permille, // P(both transmit) x 1000
 
  // The single most useful output: this link works, and here is what
  // it is one load spike away from.
  output logic        predicted_duplex_mismatch,
  output logic        predicted_no_fallback,
 
  output logic [CNT_W-1:0] c_predictions,
  output logic             ever_predicted,
  output logic             ever_exposed_duplex
);
 
  logic [N_CAUSES-1:0] exp_c;
  logic [2:0]          cnt_c;
  logic [13:0]         sim_c;
 
  always_comb begin
    exp_c = '0;
 
    // DUPLEX ASSUMED rather than agreed: exposed to a mismatch and to
    // the forced-partner case, both of which need the far end to
    // confirm and neither of which has produced a symptom yet.
    if (duplex_is_assumed || ever_parallel_detected) begin
      exp_c[C_DUPLEX_MISMATCH] = 1'b1;
      exp_c[C_ONE_END_FORCED]  = 1'b1;
    end
 
    // A COMMON SET OF ONE has no fallback: the next lost pulse takes
    // the link DOWN rather than a step down.
    if (common_count == 4'd1) exp_c[C_LOST_PULSE] = 1'b1;
 
    if (ever_over_advertised)   exp_c[C_OVER_ADVERTISE] = 1'b1;
    if (stale_measurement_used) exp_c[C_STALE_MEASURE]  = 1'b1;
    if (detection_used)         exp_c[C_SLOW_NEGOTIATOR] = 1'b1;
 
    cnt_c = 3'd0;
    for (int c = 0; c < N_CAUSES; c = c + 1)
      if (exp_c[c]) cnt_c = cnt_c + 3'd1;
 
    // P(both ends transmit in the same slot), in per mille. The number
    // that says WHEN a latent mismatch becomes visible: at 1% each way
    // it is 1 in 10 000 slots, at 50% it is 1 in 4.
    sim_c = 14'((tx_utilisation_percent * rx_utilisation_percent) / 7'd10);
  end
 
  always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n || clear) begin
      exposed_causes <= '0; exposure_count <= 3'd0;
      link_is_exposed <= 1'b0; provenance_strength <= 2'd3;
      simultaneity_permille <= 14'd0;
      predicted_duplex_mismatch <= 1'b0; predicted_no_fallback <= 1'b0;
      if (!rst_n) begin
        c_predictions <= '0; ever_predicted <= 1'b0;
        ever_exposed_duplex <= 1'b0;
      end
    end else begin
      exposed_causes        <= exp_c;
      exposure_count        <= cnt_c;
      link_is_exposed       <= (cnt_c != 3'd0);
      simultaneity_permille <= sim_c;
 
      // PROVENANCE STRENGTH, from strongest to weakest. Not an error
      // scale -- a confidence scale about how the configuration was
      // arrived at.
      if (stale_measurement_used)        provenance_strength <= 2'd0;
      else if (duplex_is_assumed)        provenance_strength <= 2'd1;
      else if (common_count == 4'd1)     provenance_strength <= 2'd2;
      else                               provenance_strength <= 2'd3;
 
      predicted_duplex_mismatch <= exp_c[C_DUPLEX_MISMATCH];
      predicted_no_fallback     <= (common_count == 4'd1);
 
      if (link_came_up) begin
        if (!(&c_predictions)) c_predictions <= c_predictions + 1'b1;
        if (cnt_c != 3'd0) ever_predicted <= 1'b1;
        if (exp_c[C_DUPLEX_MISMATCH]) ever_exposed_duplex <= 1'b1;
      end
    end
  end
 
endmodule

Classification: synthesizable.

What it teaches: that a prediction is made from provenance and its urgency from utilisation, and the two are separate inputs. duplex_is_assumed says the link is exposed; simultaneity_permille says how soon it will show. A link with assumed duplex at 1% utilisation produces a symptom roughly once in ten thousand slots — which is why it passes commissioning — and the same link at 50% produces one in four.

Deliberately simplified: the simultaneity estimate is a product of two utilisation percentages. Real traffic is bursty and correlated, so the true rate is higher than independence predicts — which makes the prediction conservative in the right direction.

Production implication: every output of this module fires on a link that is working perfectly, and that is the point. predicted_duplex_mismatch asserts at bring-up, weeks before the first symptom, on a link with zero errors and full throughput. No error-driven diagnostic can do that, because there is nothing yet to count — and by the time there is, the link has been in production long enough for the change that caused it to be forgotten.

9. Predicting Before the Load Arrives

Compute why a duplex mismatch hides, because the arithmetic explains the whole industry history of the fault.

A mismatch produces a symptom only when both ends transmit simultaneously. Treating the two directions as independent:

Utilisation each wayP(simultaneous)One event per
1%0.01 × 0.01 = 0.000110 000 slots
5%0.0025400 slots
20%0.0425 slots
50%0.254 slots

At commissioning utilisation the fault is essentially invisible. A ping test, a link-up check and a small file transfer all pass — because they never put both ends on the wire at the same instant.

At production utilisation it is continuous. The same link, unchanged, with the same configuration, now produces late collisions at one end and FCS errors at the other on a quarter of its slots.

And the interval between those two states is typically weeks, which is long enough for the configuration change that caused it to be out of anybody's change log.

The provenance was available in milliseconds.

At bring-upOn a perfect linkWhat it predicts
duplex_is_assumedseta mismatch, under load
ever_parallel_detectedsetthe same, across bounces
common_count == 1setno fallback; the next loss is an outage
ever_over_advertisedseta link that will fail to establish
stale_measurement_usedsetcorruption on a link reporting success

Five bits, none of them an error, all of them available before the first frame.

10. RTL 4 — Detecting a Renegotiation Storm

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// SYNTHESIZABLE.
//
// Detects a link that keeps renegotiating, and -- more usefully --
// measures what fraction of the time it is actually usable.
//
// WHY A DUTY CYCLE AND NOT A COUNT. A renegotiation count says a link
// is unstable. It does not say whether the link is 95% usable or 30%
// usable, and those are completely different findings:
//
//   bring-up 350 ms, renegotiating every 30 s -> 98.8% up
//   bring-up 350 ms, renegotiating every  2 s -> 82.5% up
//   bring-up 1400 ms, renegotiating every 2 s -> 30.0% up
//
// The last one is a link that spends most of its life negotiating, and
// its throughput is a third of nominal with NO errors anywhere --
// which is Section 3's "zero errors, low throughput" symptom arriving
// from a cause nobody suspects.
//
// AND THE BRING-UP TIME MATTERS AS MUCH AS THE INTERVAL. Two links
// renegotiating every 2 s differ by a factor of nearly three in usable
// throughput purely because one takes 350 ms to come up and the other
// 1400 ms -- which is Chapter 11.3's dominant_stage, again.
module renegotiation_storm_detector
  import diag_pkg::*;
#(
  parameter int unsigned CLK_MHZ = 25,
  parameter int unsigned WINDOW_S = 60,
  parameter int unsigned CNT_W = 16
) (
  input  logic clk,
  input  logic rst_n,
  input  logic clear,
 
  input  logic link_came_up,
  input  logic link_went_down,
  input  logic [15:0] last_bringup_ms,
 
  output logic [CNT_W-1:0] renegotiations_in_window,
  output logic [15:0]      mean_interval_ms,
  output logic [15:0]      shortest_interval_ms,
 
  // The number that matters: what fraction of the window the link was
  // actually usable.
  output logic [6:0]       duty_percent,
  output logic [15:0]      up_ms_in_window,
  output logic [15:0]      down_ms_in_window,
 
  output logic storm_detected,
  output logic [1:0] storm_severity,      // 0 none .. 3 severe
  output logic window_valid,
 
  output logic [CNT_W-1:0] c_storms,
  output logic             ever_storm
);
 
  localparam int unsigned MS_TICKS  = 1000 * CLK_MHZ;
  localparam int unsigned WINDOW_MS = WINDOW_S * 1000;
  // More than this many renegotiations in the window is a storm.
  localparam int unsigned STORM_COUNT = 8;
 
  logic [31:0] tick_q;
  logic [15:0] window_ms_q;
  logic [15:0] since_up_q;
  logic [CNT_W-1:0] count_q;
  logic [31:0] up_accum_q;
  logic        is_up_q;
 
  assign renegotiations_in_window = count_q;
 
  always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n || clear) begin
      tick_q <= '0; window_ms_q <= 16'd0; since_up_q <= 16'd0;
      count_q <= '0; up_accum_q <= '0; is_up_q <= 1'b0;
      mean_interval_ms <= 16'd0; shortest_interval_ms <= 16'hFFFF;
      duty_percent <= 7'd100; up_ms_in_window <= 16'd0;
      down_ms_in_window <= 16'd0;
      storm_detected <= 1'b0; storm_severity <= 2'd0;
      window_valid <= 1'b0;
      if (!rst_n) begin c_storms <= '0; ever_storm <= 1'b0; end
    end else begin
      if (tick_q == 32'(MS_TICKS - 1)) begin
        tick_q <= '0;
        if (window_ms_q != 16'hFFFF) window_ms_q <= window_ms_q + 16'd1;
        if (since_up_q  != 16'hFFFF) since_up_q  <= since_up_q + 16'd1;
        // Accumulate only the time the link was USABLE, which is the
        // whole point -- a link that is negotiating is not carrying
        // traffic, however healthy its state machine looks.
        if (is_up_q) up_accum_q <= up_accum_q + 1'b1;
      end else begin
        tick_q <= tick_q + 1'b1;
      end
 
      if (link_came_up) begin
        is_up_q <= 1'b1;
        if (since_up_q < shortest_interval_ms)
          shortest_interval_ms <= since_up_q;
        since_up_q <= 16'd0;
        if (!(&count_q)) count_q <= count_q + 1'b1;
      end
 
      if (link_went_down) is_up_q <= 1'b0;
 
      if (window_ms_q >= 16'(WINDOW_MS)) begin
        window_valid    <= 1'b1;
        up_ms_in_window <= 16'(up_accum_q);
        down_ms_in_window <= 16'(WINDOW_MS) - 16'(up_accum_q);
        duty_percent    <= 7'((up_accum_q * 100) / 32'(WINDOW_MS));
        mean_interval_ms <= (count_q == 0) ? 16'hFFFF
                            : 16'(WINDOW_MS / 32'(count_q));
 
        storm_detected <= (count_q > CNT_W'(STORM_COUNT));
        if (count_q > CNT_W'(STORM_COUNT)) begin
          ever_storm <= 1'b1;
          if (!(&c_storms)) c_storms <= c_storms + 1'b1;
        end
 
        // SEVERITY BY DUTY CYCLE, not by count. A link renegotiating
        // often but coming up in 350 ms may still be 98% usable; one
        // renegotiating less often but taking 1400 ms each time may be
        // 30% usable, and the second is far worse.
        if      (7'((up_accum_q * 100) / 32'(WINDOW_MS)) < 7'd50) storm_severity <= 2'd3;
        else if (7'((up_accum_q * 100) / 32'(WINDOW_MS)) < 7'd80) storm_severity <= 2'd2;
        else if (count_q > CNT_W'(STORM_COUNT))                   storm_severity <= 2'd1;
        else                                                      storm_severity <= 2'd0;
 
        window_ms_q <= 16'd0;
        up_accum_q  <= '0;
        count_q     <= '0;
      end
    end
  end
 
endmodule

Classification: synthesizable.

What it teaches: that severity belongs to the duty cycle and not to the renegotiation count, and the two rank links differently. A link renegotiating every 30 seconds with a 350 ms bring-up is 98.8% usable — annoying, and almost certainly not what a user is complaining about. A link renegotiating every 2 seconds with a 1400 ms bring-up is 30% usable, and its throughput is a third of nominal with no errors anywhere.

Deliberately simplified: a fixed window and integer percentages. Production detectors use a sliding window and report the distribution of intervals, because a link that renegotiates in bursts is a different fault from one that renegotiates steadily.

Production implication: the 30%-duty case is Section 3's zero errors, low throughput symptom arriving from a cause almost nobody suspects. Every frame that crosses is perfect; every counter is clean; and two thirds of the wall-clock time the link is negotiating rather than carrying traffic. A throughput investigation that looks only at frame counters finds nothing at all, because the frames that were never offered are not counted anywhere.

11. RTL 5 — Remote Fault

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// SYNTHESIZABLE.
//
// Handles the Remote Fault bit, which is the only mechanism in
// autonegotiation by which one end tells the other that something is
// wrong -- and it is one bit with no reason code.
//
// WHAT RF MEANS: "I have a fault." Nothing about which fault, which
// direction, or whether it is transient.
//
// WHICH MAKES ITS HANDLING A JUDGEMENT. Acting on it aggressively --
// tearing the link down -- turns a far-end transient into a local
// outage. Ignoring it discards the ONLY inbound signal the protocol
// offers. So this module neither acts nor ignores: it RECORDS, with
// enough context that the pattern can be read later.
//
// AND THE PATTERN IS THE INFORMATION. A single RF is noise. RF asserted
// continuously is a partner with a persistent problem. RF that appears
// only after this end renegotiates is a partner reacting to US, which
// points at a loop rather than at a fault.
module remote_fault_handler
  import diag_pkg::*;
#(
  parameter int unsigned CLK_MHZ = 25,
  parameter int unsigned CNT_W = 16
) (
  input  logic clk,
  input  logic rst_n,
  input  logic clear,
 
  input  logic remote_fault_bit,
  input  logic word_valid,
  input  logic local_renegotiated,
  input  logic link_up,
 
  output logic rf_asserted,
  output logic rf_persistent,
  output logic rf_follows_our_renegotiation,
 
  output logic [15:0]      rf_duration_ms,
  output logic [15:0]      longest_rf_ms,
  output logic [CNT_W-1:0] c_rf_assertions,
  output logic [CNT_W-1:0] c_rf_after_reneg,
 
  // The judgement, exported rather than acted on: this design does not
  // tear the link down, and it says clearly whether somebody should.
  output logic [1:0] rf_recommendation,   // 0 ignore, 1 watch, 2 investigate
  output logic       ever_rf,
  output logic       ever_rf_persistent
);
 
  localparam int unsigned MS_TICKS = 1000 * CLK_MHZ;
  // RF held this long is persistent rather than transient.
  localparam int unsigned PERSIST_MS = 5000;
  // RF within this window of our own renegotiation is a reaction.
  localparam int unsigned REACTION_MS = 500;
 
  logic [31:0] tick_q;
  logic [15:0] rf_ms_q;
  logic [15:0] since_reneg_q;
  logic        rf_q;
 
  always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n || clear) begin
      tick_q <= '0; rf_ms_q <= 16'd0; since_reneg_q <= 16'hFFFF;
      rf_q <= 1'b0;
      rf_asserted <= 1'b0; rf_persistent <= 1'b0;
      rf_follows_our_renegotiation <= 1'b0;
      rf_duration_ms <= 16'd0; longest_rf_ms <= 16'd0;
      rf_recommendation <= 2'd0;
      if (!rst_n) begin
        c_rf_assertions <= '0; c_rf_after_reneg <= '0;
        ever_rf <= 1'b0; ever_rf_persistent <= 1'b0;
      end
    end else begin
      if (tick_q == 32'(MS_TICKS - 1)) begin
        tick_q <= '0;
        if (rf_q && (rf_ms_q != 16'hFFFF))            rf_ms_q <= rf_ms_q + 16'd1;
        if (since_reneg_q != 16'hFFFF) since_reneg_q <= since_reneg_q + 16'd1;
      end else begin
        tick_q <= tick_q + 1'b1;
      end
 
      if (local_renegotiated) since_reneg_q <= 16'd0;
 
      if (word_valid) begin
        if (remote_fault_bit && !rf_q) begin
          rf_q        <= 1'b1;
          rf_asserted <= 1'b1;
          rf_ms_q     <= 16'd0;
          ever_rf     <= 1'b1;
          if (!(&c_rf_assertions)) c_rf_assertions <= c_rf_assertions + 1'b1;
 
          // RF ARRIVING JUST AFTER WE RENEGOTIATED is the partner
          // reacting to us, not reporting an independent fault -- and
          // it points at a loop rather than at a far-end problem.
          if (since_reneg_q < 16'(REACTION_MS)) begin
            rf_follows_our_renegotiation <= 1'b1;
            if (!(&c_rf_after_reneg)) c_rf_after_reneg <= c_rf_after_reneg + 1'b1;
          end
 
        end else if (!remote_fault_bit && rf_q) begin
          rf_q           <= 1'b0;
          rf_asserted    <= 1'b0;
          rf_persistent  <= 1'b0;
          rf_duration_ms <= rf_ms_q;
          if (rf_ms_q > longest_rf_ms) longest_rf_ms <= rf_ms_q;
        end
      end
 
      if (rf_q && (rf_ms_q > 16'(PERSIST_MS))) begin
        rf_persistent      <= 1'b1;
        ever_rf_persistent <= 1'b1;
      end
 
      // THE RECOMMENDATION, published rather than enacted. Tearing a
      // link down on a far-end transient turns somebody else's problem
      // into an outage here.
      if (rf_persistent)                         rf_recommendation <= 2'd2;
      else if (rf_q && !rf_follows_our_renegotiation) rf_recommendation <= 2'd1;
      else                                        rf_recommendation <= 2'd0;
    end
  end
 
endmodule

Classification: synthesizable.

What it teaches: that rf_follows_our_renegotiation distinguishes a report from a reaction, and the two point in opposite directions. Remote Fault arriving within half a second of this end renegotiating is the partner responding to our link event — which means the fault is a loop between the two devices rather than an independent problem at the far end. Chasing it at the far end finds nothing, because the far end is behaving correctly in response to something this end did.

Deliberately simplified: one persistence threshold and one reaction window. Production handlers also correlate RF with the far end's own renegotiation counter, which distinguishes a reaction from a coincidence.

Production implication: the module publishes a recommendation and never acts. Tearing a link down because the partner asserted RF converts a far-end transient into a local outage, and the protocol gives no reason code to justify it — RF is one bit meaning "I have a fault", with nothing about which fault, which direction, or whether it will clear. Acting on one bit of unqualified information is how a diagnostic becomes a fault of its own.

12. RTL 6 — History That Survives the Recovery

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// SYNTHESIZABLE.
//
// Records failure history across link bounces, because the evidence a
// diagnosis needs is destroyed by the recovery that follows the fault.
//
// THE PROBLEM. A link that fails renegotiates and usually succeeds. So
// by the time anybody looks:
//   - every window counter has been reset by the successful attempt
//   - the classifier's consistent set has been re-initialised
//   - the provenance of the CURRENT link is fine, because the current
//     link came up cleanly
//
// And the fault that mattered happened two link-ups ago.
//
// SO THIS MODULE keeps a per-cause history that survives clears and
// link events, plus the full evidence record of the WORST attempt --
// worst by exposure count rather than by recency, because the most
// recent failure is usually the least interesting one.
module failure_history_recorder
  import diag_pkg::*;
#(
  parameter int unsigned CNT_W = 12,
  parameter int unsigned CLK_MHZ = 25
) (
  input  logic clk,
  input  logic rst_n,
  input  logic clear,
 
  input  logic link_came_up,
  input  logic link_went_down,
 
  input  logic [N_CAUSES-1:0] consistent_causes,
  input  logic [N_CAUSES-1:0] exposed_causes,
  input  logic [2:0]          exposure_count,
  input  logic [1:0]          provenance_strength,
  input  logic                diagnosis_reached,
  input  logic [2:0]          diagnosis_cause,
  input  logic                remote_evidence_missing,
  input  logic [3:0]          resolved_tech,
 
  // Per-cause history. How many link-ups have been consistent with,
  // or exposed to, each cause -- across every bounce since reset.
  output logic [CNT_W-1:0] consistent_history [N_CAUSES],
  output logic [CNT_W-1:0] exposed_history    [N_CAUSES],
  output logic [CNT_W-1:0] diagnosed_history  [N_CAUSES],
 
  // The WORST link-up seen, held whole. Worst by exposure count, not
  // by recency -- the most recent link-up is usually the successful
  // retry, and the one before it is the interesting one.
  output logic             worst_valid,
  output logic [2:0]       worst_exposure_count,
  output logic [1:0]       worst_provenance,
  output logic [N_CAUSES-1:0] worst_exposed,
  output logic [3:0]       worst_tech,
  output logic             worst_had_no_remote,
 
  output logic [CNT_W-1:0] c_link_ups,
  output logic [CNT_W-1:0] c_link_downs,
  // The dominant cause across history: which one has been consistent
  // with the evidence most often. A distribution, not an event.
  output logic [2:0]       dominant_cause,
  output logic             dominant_valid
);
 
  integer i;
  logic [CNT_W-1:0] best_c;
  logic [2:0]       best_idx_c;
 
  always_comb begin
    best_c     = '0;
    best_idx_c = 3'd0;
    for (int c = 0; c < N_CAUSES; c = c + 1)
      if (consistent_history[c] > best_c) begin
        best_c     = consistent_history[c];
        best_idx_c = 3'(c);
      end
  end
 
  always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
      for (i = 0; i < N_CAUSES; i = i + 1) begin
        consistent_history[i] <= '0;
        exposed_history[i]    <= '0;
        diagnosed_history[i]  <= '0;
      end
      worst_valid <= 1'b0; worst_exposure_count <= 3'd0;
      worst_provenance <= 2'd3; worst_exposed <= '0;
      worst_tech <= 4'd0; worst_had_no_remote <= 1'b0;
      c_link_ups <= '0; c_link_downs <= '0;
      dominant_cause <= 3'd0; dominant_valid <= 1'b0;
    end else if (clear) begin
      c_link_ups <= '0; c_link_downs <= '0;
      // THE HISTORIES SURVIVE. A clear is what happens immediately
      // before somebody investigates, and clearing the history is
      // exactly what destroys the evidence they came for.
    end else begin
      if (link_went_down) begin
        if (!(&c_link_downs)) c_link_downs <= c_link_downs + 1'b1;
      end
 
      if (link_came_up) begin
        if (!(&c_link_ups)) c_link_ups <= c_link_ups + 1'b1;
 
        for (i = 0; i < N_CAUSES; i = i + 1) begin
          if (consistent_causes[i] && !(&consistent_history[i]))
            consistent_history[i] <= consistent_history[i] + 1'b1;
          if (exposed_causes[i] && !(&exposed_history[i]))
            exposed_history[i] <= exposed_history[i] + 1'b1;
        end
 
        if (diagnosis_reached && !(&diagnosed_history[diagnosis_cause]))
          diagnosed_history[diagnosis_cause] <=
            diagnosed_history[diagnosis_cause] + 1'b1;
 
        // WORST BY EXPOSURE, not by recency. A weaker provenance also
        // wins -- a link that came up on a stale measurement is worse
        // than one exposed to two causes with strong provenance.
        if (!worst_valid ||
            (exposure_count > worst_exposure_count) ||
            (provenance_strength < worst_provenance)) begin
          worst_valid          <= 1'b1;
          worst_exposure_count <= exposure_count;
          worst_provenance     <= provenance_strength;
          worst_exposed        <= exposed_causes;
          worst_tech           <= resolved_tech;
          worst_had_no_remote  <= remote_evidence_missing;
        end
 
        dominant_cause <= best_idx_c;
        dominant_valid <= (best_c != '0);
      end
    end
  end
 
endmodule

Classification: synthesizable.

What it teaches: that the worst attempt must be selected by exposure rather than by recency, and this is the module's one non-obvious decision. A link that fails renegotiates and usually succeeds, so the most recent link-up is the successful retry — and a recorder holding "the last attempt" holds the least interesting one. Ranking by exposure count and provenance strength keeps the attempt that actually had a problem.

Deliberately simplified: one worst record and three per-cause histograms. Production recorders keep a small ring of full records, each timestamped, so a pattern over hours can be reconstructed.

Production implication: the histories survive clear, and the reason is behavioural rather than technical. Clearing the counters is what an operator does immediately before running a test — and it destroys precisely the evidence they came for. consistent_history and dominant_cause turn a sequence of individually-ambiguous link-ups into a distribution: forty link-ups consistent with a duplex mismatch and three with a lost pulse is a finding, and no single link-up could have produced it.

13. RTL 7 — Reporting a Diagnosis Honestly

A diagnosis reporter takes the set of causes still consistent with the observed evidence, the set the link is exposed to from its provenance, and whether remote evidence was available. It emits three things rather than one. The first is the narrowed cause set, which may still have several members. The second is a confidence, which is high only when a single cause remains and remote evidence was available. And the third, which is the most useful and the most often omitted, is the specific observation that would discriminate among the causes that remain, so that an operator is told what to measure next rather than being given a guess presented as an answer.Consistent setmay have 4 membersExposure setfrom provenanceReportercombines bothCause setnarrowed, not decidedConfidencelow without remoteWhat to measure nextthe useful output12
Figure 3 — a diagnosis carries the cause, the confidence, and what evidence would resolve the remainder.
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// SYNTHESIZABLE.
//
// Publishes a diagnosis, and refuses to publish one it has not earned.
//
// THREE OUTPUTS, and the third is the one designs omit:
//   1. the CAUSE SET still consistent with the evidence
//   2. the CONFIDENCE, which is high only when one cause remains AND
//      remote evidence was available
//   3. WHAT TO MEASURE NEXT -- the specific observation that would
//      discriminate among the causes that remain
//
// The third turns a diagnostic from a guess into an instruction. A
// reporter that emits "probably a duplex mismatch" and a reporter that
// emits "duplex mismatch or one end forced; read the far end's
// collision counter to separate them" are the same silicon and
// completely different tools.
module diagnosis_reporter
  import diag_pkg::*;
#(
  parameter int unsigned CNT_W = 16
) (
  input  logic clk,
  input  logic rst_n,
 
  input  logic [N_CAUSES-1:0] consistent_causes,
  input  logic [3:0]          consistent_count,
  input  logic [N_CAUSES-1:0] exposed_causes,
  input  logic                remote_evidence_missing,
  input  logic                inconsistent_evidence,
  input  logic [1:0]          provenance_strength,
  input  logic [7:0]          symptoms_observed,
  input  logic                report_request,
 
  output logic [N_CAUSES-1:0] reported_causes,
  output logic [3:0]          reported_count,
  output logic [1:0]          confidence,     // 0 none .. 3 high
  output logic                report_valid,
 
  // The instruction. Which observation would most reduce the remaining
  // set, expressed as a symptom index to go and measure.
  output logic [2:0]          suggested_observation,
  output logic                suggestion_valid,
  output logic                needs_remote_evidence,
 
  // Refused to report. Distinct from reporting "unknown", because the
  // two mean opposite things: this one says the evidence CONTRADICTS
  // every cause in the taxonomy.
  output logic                report_refused,
  output logic [1:0]          refusal_reason,
 
  output logic [CNT_W-1:0] c_reports,
  output logic [CNT_W-1:0] c_refusals,
  output logic [CNT_W-1:0] c_low_confidence
);
 
  // Which causes need the far end (Section 7's table).
  localparam logic [N_CAUSES-1:0] NEEDS_REMOTE =
    (1 << C_DUPLEX_MISMATCH) | (1 << C_TABLE_DISAGREE) | (1 << C_ONE_END_FORCED);
 
  logic needs_remote_c;
  assign needs_remote_c = |(consistent_causes & NEEDS_REMOTE);
 
  always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
      reported_causes <= '0; reported_count <= 4'd0;
      confidence <= 2'd0; report_valid <= 1'b0;
      suggested_observation <= 3'd0; suggestion_valid <= 1'b0;
      needs_remote_evidence <= 1'b0;
      report_refused <= 1'b0; refusal_reason <= 2'd0;
      c_reports <= '0; c_refusals <= '0; c_low_confidence <= '0;
    end else begin
      report_valid     <= 1'b0;
      report_refused   <= 1'b0;
      suggestion_valid <= 1'b0;
 
      if (report_request) begin
        if (inconsistent_evidence) begin
          // THE EVIDENCE CONTRADICTS EVERY CAUSE. Not "unknown" --
          // the opposite. Two faults at once, an incomplete taxonomy,
          // or a wrong observation, and all three matter.
          report_refused <= 1'b1;
          refusal_reason <= 2'd1;
          if (!(&c_refusals)) c_refusals <= c_refusals + 1'b1;
 
        end else if (consistent_count == 4'(N_CAUSES)) begin
          // Nothing has been narrowed at all. Reporting the whole
          // taxonomy as a diagnosis would be technically true and
          // useless.
          report_refused <= 1'b1;
          refusal_reason <= 2'd2;
          if (!(&c_refusals)) c_refusals <= c_refusals + 1'b1;
 
        end else begin
          reported_causes       <= consistent_causes | exposed_causes;
          reported_count        <= consistent_count;
          report_valid          <= 1'b1;
          needs_remote_evidence <= needs_remote_c && remote_evidence_missing;
          if (!(&c_reports)) c_reports <= c_reports + 1'b1;
 
          // CONFIDENCE. High requires BOTH a single remaining cause and
          // remote evidence when the remaining cause needs it -- which
          // is the combination a naive reporter skips.
          if ((consistent_count == 4'd1) &&
              !(needs_remote_c && remote_evidence_missing)) begin
            confidence <= 2'd3;
          end else if (consistent_count == 4'd1) begin
            // One cause left, but it is one that needs the far end and
            // we never saw the far end. Not high confidence.
            confidence <= 2'd1;
            if (!(&c_low_confidence)) c_low_confidence <= c_low_confidence + 1'b1;
          end else if (consistent_count <= 4'd2) begin
            confidence <= 2'd2;
          end else begin
            confidence <= 2'd1;
            if (!(&c_low_confidence)) c_low_confidence <= c_low_confidence + 1'b1;
          end
 
          // THE INSTRUCTION. Suggest the observation that would most
          // reduce what remains -- ranked by how many causes it splits.
          if (needs_remote_c && remote_evidence_missing) begin
            suggested_observation <= 3'(S_ONE_SIDED_COLL);
            suggestion_valid      <= 1'b1;
          end else if (consistent_causes[C_LOST_PULSE] &&
                       consistent_causes[C_SLOW_NEGOTIATOR]) begin
            // Both are local and provenance separates them.
            suggested_observation <= 3'(S_RENEG_STORM);
            suggestion_valid      <= 1'b1;
          end else if (consistent_count > 4'd1) begin
            suggested_observation <= 3'(S_LATE_COLLISION);
            suggestion_valid      <= 1'b1;
          end
        end
      end
    end
  end
 
endmodule

Classification: synthesizable.

What it teaches: that confidence must account for which cause remains, not only how many. A single remaining cause looks decisive — and if that cause is one of the three requiring the far end, and the far end was never observed, the narrowing was done by elimination on evidence that could not eliminate it. confidence drops to 1 in exactly that case, which is the combination a naive reporter treats as its best result.

Deliberately simplified: the suggestion logic is a short priority chain. A production reporter computes which observation maximally splits the remaining set, which is a small information-gain calculation over Section 3's matrix.

Production implication: report_refused with refusal_reason = 1 is the most valuable output this module produces and the one most designs never emit. The evidence contradicting every known cause means the taxonomy is incomplete, two faults are present simultaneously, or an observation is wrong — and every one of those is a finding. A reporter that emits "unknown" in that case has merged its most informative outcome with its least.

14. The Decision Procedure

Put the mechanisms together as a procedure an operator or a script can follow.

Step 1 — read provenance, before anything else. Four causes are settled or excluded here, on a link that is working, in the milliseconds after it came up.

ReadIf setExcludes
ever_over_advertisedcause 5
stale_measurement_usedcause 6
duplex_is_assumed / ever_parallel_detectedexposed to 1, 4, 7
common_count > 1cause 2, for now
all clearcauses 2, 4, 5, 6

Step 2 — read the symptom, and expect it to narrow little. Speed below expectation leaves four; zero errors, low throughput leaves five. Only link never establishes is decisive, and it names cause 5.

Step 3 — check the duty cycle. A renegotiation storm splits causes 2 and 5 from everything else, and a 30% duty cycle explains zero errors, low throughput without any other cause being present at all.

Step 4 — get the far end. Three causes are unresolvable without it, and no amount of local evidence changes that.

CompareIfThen
collisionsone end onlycause 1 or 7
collisionsboth endsexcludes 1 and 7 — a genuinely shared segment
resolved technologydifferscause 3, and nothing else produces it
forced configurationone end forcedcause 7

Step 5 — report the set, the confidence, and what is still missing. If the remaining causes include one that needs the far end and the far end was not read, confidence is low regardless of how few causes remain.

15. Properties Worth Asserting, and One Worth Refusing

A diagnostic's properties are unusual: they are about the shape of an inference rather than about a mechanism's behaviour.

The classifier

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// P1. THE MONOTONICITY PROPERTY. The consistent set only ever shrinks.
// Evidence does not un-happen, and a set that re-grows will oscillate
// as a load-dependent symptom comes and goes.
property p_set_narrows_monotonically;
  @(posedge clk) disable iff (!rst_n || clear)
  symptom_valid |=> ((consistent_causes & $past(consistent_causes))
                     == consistent_causes);
endproperty
a_set_narrows: assert property (p_set_narrows_monotonically);
 
// P2. Observations accumulate rather than sample -- a duplex mismatch
// produces FCS errors only under load, so the symptom disappears at
// night and a sampling classifier forgets.
property p_symptoms_accumulate;
  @(posedge clk) disable iff (!rst_n || clear)
  symptom_valid |=> ((symptoms_observed & $past(symptoms_observed))
                     == $past(symptoms_observed));
endproperty
a_symptoms_accumulate: assert property (p_symptoms_accumulate);
 
// P3. A cause is eliminated only by an observation INCONSISTENT with
// it -- never by ranking, likelihood or convenience.
property p_elimination_requires_inconsistency;
  @(posedge clk) disable iff (!rst_n || clear)
  (symptom_valid && !consistent_with(S_ONE_SIDED_COLL)[C_LOST_PULSE] &&
   symptom_seen[S_ONE_SIDED_COLL]) |=> !consistent_causes[C_LOST_PULSE];
endproperty
a_elimination_needs_inconsistency: assert property (p_elimination_requires_inconsistency);
 
// P4. A DIAGNOSIS is exactly one cause. Never "the most likely of
// three" reported as an answer.
property p_diagnosis_is_singleton;
  @(posedge clk) disable iff (!rst_n)
  diagnosis_reached |-> (consistent_count == 4'd1);
endproperty
a_diagnosis_singleton: assert property (p_diagnosis_is_singleton);
 
// P5. An EMPTY set is reported distinctly from an unnarrowed one.
// Contradiction and ignorance are opposite findings.
property p_empty_set_distinct;
  @(posedge clk) disable iff (!rst_n)
  (consistent_count == 4'd0) |-> inconsistent_evidence;
endproperty
a_empty_distinct: assert property (p_empty_set_distinct);
 
// P6. The classifier starts with EVERY cause. Starting from a
// suspicion has narrowed on no evidence at all.
property p_starts_unnarrowed;
  @(posedge clk) disable iff (!rst_n)
  $rose(clear) |=> (consistent_causes == {N_CAUSES{1'b1}});
endproperty
a_starts_unnarrowed: assert property (p_starts_unnarrowed);

Two-ended correlation

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// P7. Missing remote evidence is REPORTED, never treated as agreement.
// "We could not tell" and "there is nothing wrong" are different
// answers.
property p_missing_remote_reported;
  @(posedge clk) disable iff (!rst_n || clear)
  !remote_valid |=> remote_evidence_missing;
endproperty
a_missing_remote_reported: assert property (p_missing_remote_reported);
 
// P8. And with no remote evidence, NOTHING is supported or excluded.
property p_no_remote_no_conclusions;
  @(posedge clk) disable iff (!rst_n || clear)
  remote_evidence_missing |-> ((causes_supported == '0) &&
                               (causes_excluded  == '0));
endproperty
a_no_remote_no_conclusions: assert property (p_no_remote_no_conclusions);
 
// P9. Asymmetric collisions support a mismatch. The decisive
// comparison, and it requires both ends by construction.
property p_asymmetric_supports_mismatch;
  @(posedge clk) disable iff (!rst_n || clear)
  (remote_valid && (local_colls_high != remote_colls_high) &&
   (local_fcs_high != remote_fcs_high))
    |=> causes_supported[C_DUPLEX_MISMATCH];
endproperty
a_asymmetric_supports: assert property (p_asymmetric_supports_mismatch);
 
// P10. BOTH ends colliding EXCLUDES a mismatch. An exclusion is as
// valuable as a positive finding and is usually the neglected half.
property p_both_colliding_excludes_mismatch;
  @(posedge clk) disable iff (!rst_n || clear)
  (remote_valid && local_colls_high && remote_colls_high)
    |=> causes_excluded[C_DUPLEX_MISMATCH];
endproperty
a_both_colliding_excludes: assert property (p_both_colliding_excludes_mismatch);
 
// P11. Differing resolved technologies support a table disagreement,
// and nothing else produces that observation.
property p_differing_resolution_supports_table;
  @(posedge clk) disable iff (!rst_n || clear)
  (remote_valid && (local_resolved_tech != remote_resolved_tech))
    |=> causes_supported[C_TABLE_DISAGREE];
endproperty
a_differing_resolution: assert property (p_differing_resolution_supports_table);

Prediction from provenance

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// P12. THE PREDICTION PROPERTY. An assumed duplex exposes the link to
// a mismatch, on a link that is working perfectly.
property p_assumed_duplex_exposes;
  @(posedge clk) disable iff (!rst_n || clear)
  duplex_is_assumed |=> exposed_causes[C_DUPLEX_MISMATCH];
endproperty
a_assumed_exposes: assert property (p_assumed_duplex_exposes);
 
// P13. A common set of one exposes the link to the next lost pulse
// taking it DOWN rather than a step down.
property p_single_option_exposes;
  @(posedge clk) disable iff (!rst_n || clear)
  (common_count == 4'd1) |=> exposed_causes[C_LOST_PULSE];
endproperty
a_single_option_exposes: assert property (p_single_option_exposes);
 
// P14. Provenance strength is ordered: a stale measurement is weaker
// than an assumption, which is weaker than a single option.
property p_provenance_ordered;
  @(posedge clk) disable iff (!rst_n || clear)
  stale_measurement_used |=> (provenance_strength == 2'd0);
endproperty
a_provenance_ordered: assert property (p_provenance_ordered);
 
// P15. Exposure is computed from PROVENANCE and never from errors --
// so it holds on a link with zero errors.
property p_exposure_independent_of_errors;
  @(posedge clk) disable iff (!rst_n || clear)
  (duplex_is_assumed && link_came_up) |=> ever_exposed_duplex;
endproperty
a_exposure_from_provenance: assert property (p_exposure_independent_of_errors);

The storm detector and remote fault

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// P16. Severity follows the DUTY CYCLE, not the count. A link
// renegotiating often with a fast bring-up may still be 98% usable.
property p_severity_from_duty;
  @(posedge clk) disable iff (!rst_n || clear)
  (window_valid && (duty_percent < 7'd50)) |-> (storm_severity == 2'd3);
endproperty
a_severity_from_duty: assert property (p_severity_from_duty);
 
// P17. Only time the link was USABLE is accumulated. A link that is
// negotiating is not carrying traffic, whatever its state machine says.
property p_duty_counts_up_time_only;
  @(posedge clk) disable iff (!rst_n || clear)
  !is_up_q |=> $stable(up_accum_q);
endproperty
a_duty_up_time_only: assert property (p_duty_counts_up_time_only);
 
// P18. RF arriving just after OUR renegotiation is marked as a
// reaction rather than an independent fault.
property p_rf_reaction_marked;
  @(posedge clk) disable iff (!rst_n || clear)
  (word_valid && remote_fault_bit && !rf_q && (since_reneg_q < 16'(REACTION_MS)))
    |=> rf_follows_our_renegotiation;
endproperty
a_rf_reaction_marked: assert property (p_rf_reaction_marked);
 
// P19. The handler RECOMMENDS and never acts. Tearing a link down on
// a far-end transient converts somebody else's problem into an outage.
property p_rf_never_tears_down;
  @(posedge clk) disable iff (!rst_n)
  rf_asserted |-> (rf_recommendation <= 2'd2);
endproperty
a_rf_never_tears_down: assert property (p_rf_never_tears_down);

History and reporting

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// P20. Histories survive a clear -- a clear is what happens
// immediately before somebody investigates.
property p_history_survives_clear;
  @(posedge clk) disable iff (!rst_n)
  (clear && (consistent_history[C_DUPLEX_MISMATCH] != '0))
    |=> (consistent_history[C_DUPLEX_MISMATCH] != '0);
endproperty
a_history_survives: assert property (p_history_survives_clear);
 
// P21. The worst record is selected by EXPOSURE, not by recency -- the
// most recent link-up is usually the successful retry.
property p_worst_by_exposure;
  @(posedge clk) disable iff (!rst_n)
  (link_came_up && (exposure_count > worst_exposure_count))
    |=> (worst_exposure_count == $past(exposure_count));
endproperty
a_worst_by_exposure: assert property (p_worst_by_exposure);
 
// P22. Confidence is high ONLY when one cause remains AND, if that
// cause needs the far end, the far end was observed.
property p_confidence_needs_remote;
  @(posedge clk) disable iff (!rst_n)
  (report_valid && (confidence == 2'd3))
    |-> !(needs_remote_c && remote_evidence_missing);
endproperty
a_confidence_needs_remote: assert property (p_confidence_needs_remote);
 
// P23. A report is refused when the evidence contradicts every cause,
// and the refusal is distinct from reporting "unknown".
property p_contradiction_refuses;
  @(posedge clk) disable iff (!rst_n)
  (report_request && inconsistent_evidence) |=> (report_refused &&
                                                 (refusal_reason == 2'd1));
endproperty
a_contradiction_refuses: assert property (p_contradiction_refuses);
 
// P24. And refused when nothing has been narrowed. Reporting the whole
// taxonomy is technically true and useless.
property p_unnarrowed_refuses;
  @(posedge clk) disable iff (!rst_n)
  (report_request && (consistent_count == 4'(N_CAUSES)))
    |=> (report_refused && (refusal_reason == 2'd2));
endproperty
a_unnarrowed_refuses: assert property (p_unnarrowed_refuses);
 
// P25. Every report with more than one cause carries a suggestion --
// the observation that would discriminate.
property p_multi_cause_report_suggests;
  @(posedge clk) disable iff (!rst_n)
  (report_valid && (reported_count > 4'd1)) |-> suggestion_valid;
endproperty
a_multi_cause_suggests: assert property (p_multi_cause_report_suggests);

16. Verification Scenarios

The classifier

  1. No symptoms — the consistent set is all seven; diagnosis_reached low.
  2. speed_below_expected alone — set narrows to four; no diagnosis.
  3. zero_errors_low_throughput alone — narrows to five. The most-reported symptom and the least informative.
  4. link_never_establishes — narrows to one: over-advertisement. The only decisive single symptom.
  5. one_sided_collisions alone — narrows to two: duplex mismatch or one end forced.
  6. one_sided_collisions then late_collisions — still two; the second observation adds nothing because their masks are identical.
  7. speed_below_expected then one_sided_collisions — intersection is one; a diagnosis, reached by two observations neither of which was decisive.
  8. A symptom asserting then de-asserting — the set does not re-grow. P1.
  9. A load-dependent symptom appearing only at peaksymptoms_observed accumulates; the classification survives the quiet period. P2.
  10. Contradictory symptomslink_never_establishes plus one_sided_collisions: the set empties, inconsistent_evidence high.
  11. clear — the set returns to all seven. P6.
  12. A classifier mutated to start from a suspicion — P6 fires.

Two-ended correlation

  1. No remote evidenceremote_evidence_missing, and causes_supported and causes_excluded both empty. P8.
  2. Collisions local, none remote; FCS remote, none local — the duplex-mismatch signature; both causes 1 and 7 supported, causes 2 and 4 excluded.
  3. Collisions at both ends — cause 1 excluded. The negative finding that unblocks an investigation.
  4. Resolved technologies differing — cause 3 supported; nothing else produces it.
  5. Both ends forced, differentlyboth_forced_differently.
  6. Local collisions of 6 with a noise floor of 8not counted as asymmetric; zero is rarely exactly zero.
  7. Remote evidence that is stale — the correlator has no way to know; documented as a limitation of the record, not of the module.

Prediction

  1. duplex_is_assumed set at bring-up, zero errorspredicted_duplex_mismatch, link_is_exposed, on a perfect link.
  2. common_count == 1predicted_no_fallback.
  3. stale_measurement_usedprovenance_strength = 0, the weakest.
  4. duplex_is_assumed with 1% utilisation each way — exposed, simultaneity_permille ≈ 0. The commissioning case.
  5. The same link at 50% each waysimultaneity_permille ≈ 250. Same exposure, imminent symptom.
  6. A clean negotiation, full common setlink_is_exposed low, provenance_strength = 3.
  7. A link that bounces and comes back cleanlyever_exposed_duplex survives.

Storms and remote fault

  1. Renegotiating every 30 s with a 350 ms bring-up — duty 98.8%, severity 0 or 1.
  2. Every 2 s with a 350 ms bring-up — duty 82.5%, severity 2.
  3. Every 2 s with a 1400 ms bring-up — duty 30%, severity 3. Same interval, opposite finding.
  4. A link up continuously — duty 100%, storm_detected low.
  5. Time spent negotiatingnot accumulated into up_accum_q. P17.
  6. RF asserted 200 ms after a local renegotiationrf_follows_our_renegotiation; a reaction, not a report.
  7. RF asserted 5 s after any local event — an independent fault; rf_recommendation = 1.
  8. RF held for 6 srf_persistent, recommendation 2.
  9. RF asserting and clearing repeatedlylongest_rf_ms records the worst; the link is not torn down. P19.

History and reporting

  1. Forty link-ups consistent with cause 1, three with cause 2dominant_cause = 1. A distribution no single link-up could produce.
  2. A failure followed by seven successesworst_* still holds the failure. P21.
  3. A link-up with three exposures after one with two — the worst record updates.
  4. A link-up with weaker provenance but fewer exposures — the worst record still updates; provenance breaks the tie.
  5. clear after a history has accumulated — counters clear, histories survive. P20.
  6. A report with one cause and remote evidence presentconfidence = 3.
  7. A report with one cause and remote evidence missing, where that cause needs the far endconfidence = 1, not 3. The combination a naive reporter treats as its best result.
  8. A report with four causesconfidence = 1, suggestion_valid high.
  9. Contradictory evidencereport_refused, reason 1.
  10. No narrowing at allreport_refused, reason 2. Reporting all seven is true and useless.

17. Debugging: The Decision Procedure in Practice

ObservationConsistent causesNext observation
link never establishesover-advertisementever_over_advertised, refused_bits — done
speed below expected, ever_parallel_detected setslow negotiator detectedc_aborts; try a longer detection window
speed below expected, common_count == 1lost pulsecompare the two ends' pulse counts
speed below expected, both clean locallytable disagreementread the far end's resolved technology
FCS errors under load, full duplex, no collisionsduplex mismatch, stale measurement, one end forcedread the far end's collision counter
the far end collides, this end does notduplex mismatch or one end forcedis the far end forced?
both ends collidea genuinely shared segment — not a mismatchcheck the topology; this is normal half duplex
zero errors, low throughputfive causesduty cycle first — a storm explains it alone
duty cycle below 50%renegotiation stormdominant_stage from 11.3; why is bring-up slow
link works, provenance_strength lownot a fault — an exposureact before the load arrives
evidence fits no causetwo faults, or a gap in the taxonomydo not guess; report the contradiction

Four habits.

First, read provenance before reading symptoms. Four of the seven causes are settled or excluded by five register bits available before any traffic — and none of those bits is an error, which is why no dashboard shows them.

Second, treat the duty cycle as a first-class explanation. Zero errors, low throughput is consistent with five causes, and a 30% duty cycle explains it entirely with no other cause present. Two thirds of the wall-clock time the link is negotiating, every frame that crosses is perfect, and the frames never offered are counted nowhere.

Third, get the far end before narrowing to a remote cause. Three causes are unresolvable locally. A diagnostic that narrows to one of the four local causes without remote evidence has excluded the other three on no evidence at all — which is precisely the reasoning that replaces a cable twice.

Fourth, when the evidence fits nothing, say so. Two simultaneous faults and an incomplete taxonomy both produce an empty consistent set, and both are findings. Naming the nearest member of a set that does not contain the answer is the one outcome worse than no answer.

18. Common Misconceptions

"A duplex mismatch shows up as errors, so an error counter will find it."

The wrong model: the fault produces a symptom continuously.

What it costs: weeks between the configuration change and the first report.

The corrected model: a mismatch produces a symptom only when both ends transmit simultaneously. At 1% utilisation each way that is roughly 0.01 × 0.01 = 0.0001one event per ten thousand slots — so the link passes commissioning, passes acceptance, and degrades in production. At 50% it is one slot in four. And the evidence that predicts it — duplex_is_assumed, ever_parallel_detectedwas available in the milliseconds after link-up, on a link with zero errors.

"FCS errors on a full-duplex port mean a duplex mismatch."

The wrong model: a symptom implies a cause.

What it costs: Section 15's rejected property, and an engineer reconfiguring duplex on a link whose real fault is elsewhere.

The corrected model: FCS errors under load are consistent with at least seven causes — three in this chapter's taxonomy and four more from earlier modules: an RGMII delay error, a swapped XAUI lane, a wrapped deskew FIFO, a nibble swap. Causes imply symptoms; symptoms do not imply causes. The heuristic is a good ranking and a false implication, and a design that reports one cause has published a ranking as an answer.

"If a diagnostic cannot find a cause, it should report the most likely one."

The wrong model: an answer is always better than no answer.

What it costs: the two most informative outcomes a classifier can produce.

The corrected model: an empty consistent set and an unnarrowed one mean opposite things. Empty means the evidence contradicts every known cause — two simultaneous faults, an incomplete taxonomy, or a wrong observation, and all three are findings. Unnarrowed means no evidence has been gathered. A reporter that emits "unknown" for both, or emits its best guess for both, has merged its most and least informative results.

"A renegotiation count tells you how bad a flapping link is."

The wrong model: more renegotiations is worse.

What it costs: two links ranked in the wrong order.

The corrected model: severity belongs to the duty cycle. A link renegotiating every 30 seconds with a 350 ms bring-up is 98.8% usable. One renegotiating every 2 seconds with a 1400 ms bring-up is 30% usable — a third of nominal throughput, with no errors anywhere, because the frames never offered are not counted. The bring-up time matters as much as the interval, which points straight back at Chapter 11.3's dominant_stage.

"Remote Fault means the far end is broken, so tear the link down."

The wrong model: one bit, one action.

What it costs: somebody else's transient becomes a local outage.

The corrected model: RF is one bit meaning I have a fault, with no reason code, no direction, and no indication of whether it will clear. And RF arriving within half a second of this end renegotiating is the partner reacting to us — a loop, not an independent fault, and chasing it at the far end finds nothing. The right response is to record with context and publish a recommendation: persistent RF is worth investigating; RF that follows our own renegotiation points at this end.

19. Interview Reasoning

"Why did duplex mismatch take the industry a decade to routinely diagnose?"

Because its evidence is asymmetric and its symptom is load-dependent, and neither end alone has enough information. The half-duplex end sees collisions, including late collisions; the full-duplex end sees FCS errors and runts and no collisions at all. Each alone tells a plausible and wrong physical-layer story — the segment is congested and the cable is badand the pair is unmistakable, because a genuinely shared segment collides at both ends. The strong answer adds the timing: a mismatch produces a symptom only on simultaneous transmission, which at 1% utilisation each way is 0.0001 — one event in ten thousand slots — so the link tests clean at commissioning and degrades weeks later, by which time the configuration change is out of the change log. The finishing point: the predictive evidence was free and immediate. duplex_is_assumed is set at bring-up, on a link with zero errors, and it is not an error, which is why no dashboard shows it.

"Which negotiation failures can a single device diagnose, and which cannot?"

Four of seven can, and every one of the four is diagnosed from provenance rather than from errors: a lost pulse (common_count against the expected advertisement), a slow negotiator parallel-detected (ever_parallel_detected), an over-advertisement (ever_over_advertised), and a stale measurement (stale_measurement_used). Three cannot, and they share a structure: each is a disagreement between two correct devices, so there is no local anomaly because the local device did exactly the right thing. A duplex mismatch needs the far end's collision counter; a table disagreement is invisible from either end because both resolved correctly against their own table, and the only observation is that the two resolved technologies differ; one end forced needs the far end's configuration. The finishing point: a device that cannot see the far end must say so, because narrowing to one of the four local causes without remote evidence has excluded the three remote ones on no evidence at all — which is the reasoning that replaces a cable twice.

"A link reports zero errors and a third of expected throughput. How do you proceed?"

Duty cycle first, because a renegotiation storm explains it entirely with no other cause present. A link renegotiating every 2 seconds with a 1400 ms bring-up is 30% usable — two thirds of the wall-clock time it is negotiating rather than carrying traffic — and every frame that does cross is perfect, so every counter is clean and the frames never offered are counted nowhere. If the duty cycle is high, the symptom is consistent with five causes and the next step is provenance: duplex_is_assumed, common_count, ever_parallel_detected. The strong answer notes what makes this symptom hard: it is the one users report and the one that discriminates least — 0.49 bits, five causes standing. And the finishing point: if a storm is the cause, the next question is Chapter 11.3's dominant_stage, because the bring-up time matters as much as the interval.

"Would you assert that FCS errors under load on a full-duplex port imply a duplex mismatch?"

No — and the property is not merely imprecise, it is false in the direction that matters. Causes imply symptoms; symptoms do not imply causes. FCS errors under load are consistent with at least seven causes — a duplex mismatch, a stale measurement, one end forced, plus an RGMII delay error, a swapped lane, a wrapped deskew FIFO and a nibble swap. The property converts a ranking of likelihoods into a logical implication, and a design built to satisfy it emits a single cause with no confidence and no alternatives — reporting the common cause and the uncommon one in identical words. Assert the shape of the inference instead: the consistent set only shrinks, observations accumulate, a cause is eliminated only by an inconsistent observation, a diagnosis is a singleton, an empty set is a contradiction reported distinctly from ignorance, confidence is high only when the far end was observed if the remaining cause needs it, and every multi-cause report names the observation that would discriminate. The test: is there any other condition that produces this same observation?

20. Understanding Check

Because the mapping from symptom to cause is many-to-many, and the direction that holds is the other one.

Causes imply symptoms. A duplex mismatch will produce collisions at one end and FCS errors at the other. That direction is sound.

Symptoms do not imply causes. FCS errors under load is produced by a duplex mismatch, a stale measurement, one end forced, an RGMII delay error, a swapped lane, a wrapped deskew FIFO and a nibble swapseven causes, one observation.

SymptomCauses consistentInformation
link never establishes12.81 bits — the only decisive one
collisions at one end only21.81 bits
FCS errors under load31.22 bits
speed below expectation40.81 bits
zero errors, low throughput50.49 bits

And the two symptoms a user actually reports are the two least informative.

So a classifier's output must be a set, narrowed by intersection as observations arrive — and a classifier that emits one cause has published a ranking as an answer, right most of the time and indistinguishable from wrong the rest.

21. What's Next

The claim this chapter defended: a symptom narrows a set of causes rather than implying one.

Seven causes make a link come up wrong, and six of them report no error at all — because in every one, each device behaved correctly against the information it had. A lost pulse produced a smaller advertisement and the resolver correctly took the highest common member of it. Two ends resolved correctly against different tables. A forced end correctly declined to negotiate. There is no wrong actor anywhere, which is why no counter fires.

And the symptoms are shared, because all seven corrupt a configuration and every configuration error is observed through the frame layer's four-word vocabulary: delivered, corrupt, collided, absent. Four outcomes for seven causes; the mapping cannot be one-to-one. The two symptoms users report — speed below expectation and zero errors, low throughput — leave four and five causes standing, while the decisive ones require the far end.

So the two mechanisms that work are not error counters. A two-ended correlator, because three causes are disagreements between correct devices and no local observation can see them — and both ends colliding excludes a mismatch, which is as valuable as any positive finding. And a provenance-driven predictor, because four causes leave a mark at bring-up, on a working link, weeks before the load that will expose them: duplex_is_assumed, ever_parallel_detected, common_count == 1, stale_measurement_used. None is an error. All predict.

Which is why the rejected property here is the one every engineer's instinct writes. FCS errors under load imply a duplex mismatch is a good ranking and a false implication — causes imply symptoms, and symptoms do not imply causes — and a design built to satisfy it reports the common cause and the uncommon one in identical words.

Module 11 is complete. Four chapters: what can be observed before anything is agreed, what is exchanged and never concluded, the eight-stage sequence and its data dependencies, and the taxonomy of ways it all goes quietly right-looking and wrong.

Chapter 12.1 — What a Switch Does opens the module that turns a set of links into a network.

Everything so far has been about one link: two devices, one cable, one negotiation. Chapter 9.2 §3 established the mechanism that made that possible — a switch port is a collision domain of one, which is what made full duplex possible and left CSMA/CD unreached. Module 12 asks what the switch itself does. How a frame arriving on one port is delivered to exactly the port that needs it, how the table that makes that possible is built from nothing but the frames themselves, what happens when the table has no answer, how the table is built in hardware and what happens when it fills — and the store-and-forward against cut-through choice that sets a switch's latency and decides whether it propagates a corrupt frame or absorbs it.

The full path is on the Ethernet curriculum index.

Continue learning

Standards & specifications

Governing standard
IEEE Std 802.3 (Ethernet)(opens IEEE in a new tab)

Defines the Ethernet MAC, the media-independent interfaces and the physical-layer sublayers, including framing, access control, auto-negotiation and per-rate PHY specifications. VLAN tagging, priority and time-sensitive shaping are defined by IEEE 802.1, not by 802.3.

This page also covers RTL structure, verification approach and debugging technique. Those are engineering practice built on the standard, not requirements the standard itself imposes.

Where this fits

Part of the Ethernet curriculum.