Skip to content
VLSI Mentor

DDR · Module 28

Calibration Failures

Five failures hide behind one done flag — never started, never converged, never committed, no longer valid, and valid but insufficient. One of them has no digital evidence at all.

Chapter 28.1 left one thing on trust. Its hypothesis C — a prerequisite step never committed — was tested by reading the prerequisite's own committed flag, and the chapter said plainly that the flag is taken on trust.

This chapter is about why that trust is misplaced, and the answer is that a calibration done flag can mean five different things.

The flag saysWhat may actually have happened
donethe routine never started — a lifecycle precondition was unmet
doneit ran and never converged, then reported completion on a timeout path
doneit converged and the result never reached the hardware
doneit committed correctly and has since stopped being valid
doneeverything above is fine and the result fails under real traffic

Five failures, one flag, and the last is the one that produces a system which boots, passes its own self-checks, and corrupts data hours later.

And there is a sixth case that is worse than all of them. Chapter 7.7 owns ZQ calibration and establishes, in its own words, the unverifiability of its result — a command you issue, whose completion you can time, and whose outcome the interface does not report. For that mechanism, "did it work?" is not a question digital logic can answer at all, and §10 is about what to do instead.

1. Five Events, Not One Flag

Chapter 19.5 opens by splitting PHY bring-up into five distinct events — reset release, clock stability, initialisation, calibration, readiness — and that split is the reason a single done flag is ambiguous.

A calibration routine sits inside that lifecycle, and it can fail at each boundary.

FailureWhere it happensCheapest evidence
Failed to starta precondition of the lifecycle was unmetthe routine's own started flag, separate from done
Failed to convergethe search ran out of range, probes, or timeprobe count and the reason field — 28.1
Failed to committhe result was computed and not appliedthe applied register read back against the computed value
Became stalea configuration changed under a valid resultthe result's provenance versus the active configuration
Fails at runtimeeverything committed and margin is insufficientnone of the above — §11

The first four are digital and cheap. The fifth is not, and that asymmetry drives the whole investigation order.

So the first instrument a calibration needs is not a better done flag — it is separate flags. started, converged, committed and valid are four different facts, and a design publishing one boolean has made four failures indistinguishable.

2. Started Is Not Done

The failure nobody instruments, because it produces the cleanest possible log.

A routine that never ran cannot fail. If done is implemented as not busy — a common and reasonable-looking choice — then a routine that never started reads as done from the first cycle after reset.

done implemented asA routine that never starts reads
!busydone, immediately
a latched completionnot done — correct
started && !busynot done — correct

CURRICULUM-DERIVED from 19.5's lifecycle: calibration has preconditions — reset released, clock stable, initialisation complete. A precondition that is never satisfied means the routine never starts, and downstream steps that check only done proceed on a value that was never computed.

This is 28.1 §3's hypothesis C from the other side. That chapter asked did the prerequisite commit? and trusted the flag; here the flag says done because nothing happened.

The discriminating evidence is a started count. Not a flag — a count, because §7 shows that how many times it started is the question a retry investigation needs. A calibration that reports done with a start count of zero is localised completely, in one read.

3. Converged Is Not Committed

The gap between computing a result and the hardware using it.

A search produces a value. Something must then write it where the datapath reads it. That write can fail in ways the search never sees:

MechanismSignature
The commit path is gated by a condition never metcomputed value present, applied register at reset default
The write targets the wrong instanceone lane correct, others at default — 28.1 §8's lane-local shape
The write happens and is overwrittenapplied value correct briefly, then default
The write is byte-swapped or mis-shiftedapplied value is a transform of the computed one
The commit is asynchronous and the datapath samples during itintermittent, and only at high traffic

Row four has a signature worth recognising because it looks like a search failure. If the applied value is a consistent transform of the computed one — shifted, truncated, byte-swapped — the search converged correctly and the plumbing is wrong. Comparing the two numbers rather than judging the applied one is what distinguishes them, and it takes one extra register read.

The general instruction is the readback. Read the applied value back from where the datapath actually reads it, and compare it against the computed value. A calibration that publishes only its computed result has made every failure in this table invisible.

4. The Lifecycle and Its Five Exits

A calibration routine drawn around its five failure exits, with eight states. The machine starts in idle and waits for its preconditions. When the preconditions are met it enters the search state, where it probes settings and evaluates them. If the preconditions are never met it takes the not-started exit, which is the failure that produces the cleanest possible log because a routine that never runs never fails. From search the machine either converges, moving to the commit state, or exhausts its probe budget or range, moving to the not-converged exit. In commit the computed result is written to the hardware and read back. If the readback matches, the machine moves to valid, where the result is in use and the routine is complete. If the readback does not match the computed value, the machine takes the commit-failed exit, which covers a gated write, a wrong target instance, an overwrite, and a transformed value. From valid there are two further exits. A configuration change that the result was not calibrated against moves the machine to the stale state, where the committed value is still applied but no longer applies to the current conditions, and the correct behaviour is to drop validity rather than continue. And a result that is committed, valid, and simply insufficient under real traffic produces no state change at all, which is why the runtime-failure exit is drawn from valid and is marked as the one failure with no digital evidence. All failure states return to idle for a retry, and the diagram's point is that a single done flag cannot distinguish the five exits.IDLESEARCHCOMMITVALIDNOTSTARTEDNOCONVERGECOMMITFAILSTALEpreconditions metpreconditions metpreconditions never metpreconditionsnever metconvergedconvergedbudget or range spentbudget or range spentreadback matchesreadback matchesreadback differsreadback differsconfig changedconfigchangedruntime failure: no exitruntime failure: no exitruntimefailure: no…recalibraterecalibraterecord and retryrecord and retryrecord and retryrecord and retryrecord and retryrecord and retry

Two things the diagram is built to show.

COMMIT has a readback test between it and VALID. §3's whole table lives on that edge, and a lifecycle that transitions straight from converged to valid has deleted the only check that catches five different plumbing failures.

And the runtime-failure exit is a self-loop on VALID, deliberately. There is no state transition for a committed result that is simply not good enough — the machine stays valid, the flag stays set, and the failure appears somewhere else entirely as data corruption. That is the honest drawing of §11's boundary, and it is why 28.5 is a separate investigation rather than a calibration result.

5. Staleness — What the Result Was Calibrated Against

Chapter 19.5 owns what it means for a trained value to become valid or stop being valid. This chapter's contribution is that a value cannot be checked for staleness unless it carries what it was calibrated against.

A committed value is a function of the conditions at calibration time. Change the conditions and the value may no longer apply — and nothing about the value itself reveals that.

Condition that changedWhy the value may no longer apply
Frequencytiming-related codes scale with the period
Voltagereference levels and drive behaviour shift
Temperature band15.4's band change, in a new domain
Device or rank seta different load and a different device
Termination configurationModule 22 — the channel the value was measured through
Warm reset without recalibration19.5's cold-versus-warm hazard

So a calibration result needs a provenance tag, and the tag must contain exactly the conditions the value depends on. A tag is not documentation — it is the comparison that makes valid computable:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   validity is not a flag you set. It is a comparison you can run.

     valid  ==  (committed_tag == current_conditions)

   a design that stores `valid` as a latched bit is asserting that
   nothing has changed since the commit -- which is exactly the
   assumption that fails. A design that stores the TAG can answer
   the question at any moment, including after a change nobody
   thought to hook up an invalidation for.

   the practical difference: an invalidation you forgot to wire
   leaves a latched bit set and a tag comparison FALSE.

That is the argument for tags over invalidation signals, and it is a debugging argument rather than a design-elegance one. Invalidation is a list of events someone had to remember; a tag comparison is a statement about the present. A missed invalidation is a silent staleness bug; a missed tag field is a comparison that is too permissive and is visible by inspecting the tag.

6. A Stale Result, Cycle by Cycle

The commit-then-change sequence, and the bit that should have fallen. ILLUSTRATIVE codes.

Committed, valid, and then quietly wrong

10 cycles
A ten-cycle calibration lifecycle trace. The clock runs throughout. A calibration start pulse occurs at cycle one. The probing signal is high across cycles two, three and four while the search evaluates settings. At cycle five the converged signal rises and stays high, and the done signal rises with it. At cycle six the committed signal rises and the applied code lane changes from undefined to forty-two, which is the value the search produced. The validity signal rises at cycle six alongside the commit, correctly, because at that moment the committed value does match the conditions it was calibrated against. At cycle eight a configuration change pulse occurs, representing a frequency or voltage change that the calibration result depends on. From cycle eight onward the applied code remains forty-two and the validity signal remains high, which is the defect the trace exists to show: the conditions changed and the validity bit did not fall, so the system continues using a value calibrated against conditions that no longer hold. The first marker at cycle five notes convergence. The second marker at cycle six notes the commit and the readback match, which is the transition that should gate validity. The third marker at cycle eight notes the configuration change and that validity should have fallen there but did not. The first phase spans cycles two to four and covers the search. The second phase spans cycles eight to nine and covers the interval during which the applied value is stale while still being reported as valid.searchsearchstale but reporting validstale butreporti…converged: a value existsconverged: a value existscommitted, readback matchedcommitted, readback matchedconfig changed: valid should fallconfig changed: validshould fallCKcal_startprobingconvergedcommittedapplied00000042424242cfg_changevalidt0t1t2t3t4t5t6t7t8t9

Everything in this trace is correct until cycle 8, and that is the difficulty. The search converged, the commit succeeded, the readback matched, and valid rose for good reasons. The defect is a non-event — a bit that did not fall — and non-events are invisible in a trace unless you know what to look for.

Under §5's tag model the same trace is self-diagnosing. valid would be computed as committed_tag == current_conditions, and the configuration change at cycle 8 makes that comparison false without anyone having wired an invalidation to this particular change. §12's block implements it that way for exactly this reason.

And notice what the trace cannot show. If the configuration had not changed, and the committed code of 42 were simply insufficient under real traffic, every signal here would be identicalvalid high, applied 42, nothing amiss. That is §11.

7. Retry Tells You Less Than You Think

Calibration is the place where the module's fourth law is most often broken, because retrying is cheap and usually works.

A second calibration pass that converges does not explain the first. Worse, the two passes ran under different conditions by construction: the second ran after the first, so the device is warmer, the supplies have settled further, and any state the first pass left behind is present.

ObservationSupportsDiscriminating experiment
Pass 1 fails, pass 2 succeeds, pass 3 succeedspass 1 ran in a different statefull reset before pass 2; if it now fails, the state was the cause
Every odd pass failsthe routine leaves state that breaks the nextrun three passes with resets between; failures should vanish
Passes succeed but commit different valuesthe result depends on uncontrolled conditionshold temperature and voltage; spread should collapse
First pass after a cold boot always fails19.5's cold-versus-warm hazardcompare cold-boot and warm-reset sequences step by step

Row three is the one to instrument rather than argue about. Run calibration N times under nominally identical conditions and record the spread of committed values. A spread of zero means the routine is deterministic; a wide spread means the result depends on something nobody is controlling — and a system that commits whichever value the boot happened to produce is one unlucky boot from a field failure.

That spread is also the honest way to talk about margin without measuring it. A routine whose committed code varies by several steps across identical boots is telling you the decision is close, in units of its own code steps — which 21.4 is explicit is not the same as volts.

8. The Side Asymmetry

Chapter 21.4 owns the direction asymmetry — which side of the link each reference level belongs to — and it produces a calibration bug with a distinctive signature.

A link has two directions and the reference levels are not interchangeable. A level used for one direction applied to the other is a legal register write producing a wrong configuration.

SymptomSupports
Reads clean, writes corrupt (or the reverse)a direction-specific setting is wrong
Both directions corrupt, one worsea shared setting is wrong and one direction has less margin
Corruption appears only after a calibration that reported successa committed value applied to the wrong side

Row one is the signature, and it is strong evidence because few other mechanisms are direction-asymmetric. Chapter 21.4 establishes that the two ranges do not overlap where it matters, so a cross-applied value is usually not merely suboptimal — it is outside the range that direction needs.

The discriminating experiment is a directional split. Run a read-only pattern and a write-only pattern separately. If one is clean and the other is not, the fault is direction-specific, which excludes most channel and most retention explanations in one run and points at the direction's own settings.

And this is the cheapest example in the module of localising before correcting. The alternative — recalibrating everything — would fix it, hide it, and leave the cross-application in place for the next configuration.

9. Continuous Calibration — Drift, Disturbance, and Never

Chapter 19.5 owns the one-time versus continuous split, and everything so far has assumed the one-time case: calibrate, commit, use. A continuously recalibrated value has three additional failure modes, and they are not variants of the five — they are new.

FailureMechanismSignature
Never recalibratesthe periodic trigger never firesa one-time value used forever; drift appears as slow degradation
Driftseach pass adjusts from the last, and the adjustments accumulate in one directionthe committed value walks monotonically over hours
Disturbsrecalibration perturbs the link while traffic is runningerrors clustered at recalibration instants — §14's histogram shape

Row one is the staleness problem with a different cause, and §5's tag comparison does not catch it. The tag still matches — the conditions have not changed — and the value is nonetheless getting worse because what it tracks is drifting underneath it. So a continuous calibration needs a cadence check exactly as 28.3's obligation does, and for the same reason: a periodic obligation that stops being served reports nothing.

Row two is the one that requires a history rather than a snapshot. A single committed value tells you nothing about drift; a sequence does. And the distinction that matters is between tracking and running away:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   committed code over eight recalibration passes, ILLUSTRATIVE

   tracking (healthy)        drifting (a finding)
   ------------------        --------------------
   41 42 41 42 43 42 41 42   41 42 44 46 49 51 54 56
   ^ oscillates about 42      ^ monotone, +15 over 8 passes

   both are "calibration succeeded" on every pass. Both commit a
   value, read back clean, and keep a matching tag. The first is a
   loop doing its job; the second is a loop whose feedback is wrong,
   or which is tracking something genuinely moving.

   §12's code_min/code_max bound the spread but do NOT separate these
   two: both have a spread of 15 if you only keep extremes. What
   separates them is MONOTONICITY, which needs the direction of the
   last few steps.

That is a real limitation of §12's instrument and worth stating plainly. code_min and code_max were introduced in §7 to bound boot-to-boot repeatability, and they are the wrong instrument for drift. A drift check needs a monotone-run counter — how many consecutive adjustments moved the same way — which is a different measurement from a spread.

Row three is the disturbance case, and it is the one most likely to be misattributed. A recalibration that runs during traffic can perturb the link for the duration of the pass. Errors then cluster at recalibration instants — which looks exactly like 28.3 §9's drain-and-restart signature, and is distinguished from it by which periodic event the errors align with. Bucketing errors by position relative to the recalibration interval, rather than the refresh interval, is the discriminating experiment, and it is the same instrument pointed at a different clock.

10. The Result You Cannot Check

Chapter 7.7 owns ZQ and establishes the unverifiability of its result: a command with a precondition and an occupancy, whose outcome the interface does not report. Its own section titles are The Result Is Not Observable and Issuing a Command You Cannot Check.

So for this mechanism, four of §1's five failures are undetectable by reading a result, because there is no result to read.

FailureDetectable for a search-based calibration?For an unverifiable one?
Failed to startyes — started countyes — the command either issued or did not
Failed to convergeyes — probe countno — there is no convergence to observe
Failed to commityes — readbackno — nothing to read back
Became staleyes — tag comparisonpartly — the conditions are still comparable
Fails at runtimenono

What survives is the left-hand column of the lifecycle: issuance, precondition, occupancy and interval.

So the instrumentation shifts from the result to the obligation, and the checkable statements become:

Was the precondition satisfied when the command issued? Chapter 7.7 owns the precondition; a command issued without it is a definite defect even though its result is unobservable.

Was the required occupancy respected? The command occupies the interface for a specified interval, and issuing traffic inside that interval is a definite defect.

Has the command issued at the required cadence? For a periodic obligation, a missed cadence is checkable exactly as 28.3's obligation is — and the same distinction applies between late and missed.

That is the general lesson and it is worth stating beyond ZQ. When a result is unobservable, verify the obligations around it rather than asserting something about the result. The alternative — treating the absence of an error as evidence of success — is 27.2 §2's vacuity argument in its purest form: a check that cannot fail.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ---------------------------------------------------------------------
// unverifiable_cal_obligations -- CLASSIFICATION: synthesisable, BINDABLE.
//
// WHAT IT DOES: §10's three checkable statements for a calibration
// whose RESULT cannot be read -- precondition at issue, occupancy
// respected, cadence maintained.
//
// WHY IT CHECKS OBLIGATIONS AND NOT THE RESULT: 7.7 owns the
// unverifiability of ZQ's result. Treating the absence of an error as
// evidence of success would be 27.2 §2's vacuity argument in its
// purest form -- a check that cannot fail. So this block asserts
// nothing about the outcome and everything about the contract.
//
// WHAT IT CANNOT TELL YOU: whether the calibration did anything. Every
// check here passes on a device that silently ignored the command.
//
// SYNTHESIS: three counters and a down-counter. No DUT handle.
// ---------------------------------------------------------------------
module unverifiable_cal_obligations #(
  parameter int OCCUPANCY = 64,     // cycles the command occupies, ILLUSTRATIVE
  parameter int CADENCE   = 4096,   // required maximum interval between issues
  parameter int CAD_W     = 24
)(
  input  logic              clk,
  input  logic              rst_n,
  input  logic              cmd_issue,      // the unverifiable command
  input  logic              precond_met,    // 7.7's precondition
  input  logic              traffic_valid,  // any other bus activity
  input  logic              clear,

  output logic              busy,
  output logic [15:0]       issues,
  output logic [15:0]       precond_violations,
  output logic [15:0]       occupancy_violations,
  output logic [15:0]       cadence_misses,
  output logic [CAD_W-1:0]  since_issue,
  output logic              ever_issued
);
  initial begin
    if (OCCUPANCY < 1) $fatal(1, "unverifiable_cal_obligations: OCCUPANCY must be >= 1");
    if (CADENCE   < 2) $fatal(1, "unverifiable_cal_obligations: CADENCE must be >= 2");
    // The interval counter must represent CADENCE, so it needs
    // $clog2(CADENCE+1) bits -- COUNT versus INDEX. Checking it here
    // rather than silently saturating below the deadline.
    if (CAD_W < $clog2(CADENCE + 1))
      $fatal(1, "unverifiable_cal_obligations: CAD_W too narrow for CADENCE");
  end

  localparam int OCC_W = (OCCUPANCY + 1 < 2) ? 1 : $clog2(OCCUPANCY + 1);
  logic [OCC_W-1:0] occ_left;

  always_ff @(posedge clk) begin
    if (!rst_n || clear) begin
      occ_left             <= '0;
      issues               <= '0;
      precond_violations   <= '0;
      occupancy_violations <= '0;
      cadence_misses       <= '0;
      since_issue          <= '0;
      ever_issued          <= 1'b0;
    end else begin
      if (cmd_issue) begin
        if (issues != 16'hFFFF) issues <= issues + 1'b1;
        ever_issued <= 1'b1;
        occ_left    <= OCCUPANCY[OCC_W-1:0];
        since_issue <= '0;
        // Obligation 1: the precondition must hold AT ISSUE. A command
        // issued without it is a definite defect even though its
        // result is unobservable.
        if (!precond_met && precond_violations != 16'hFFFF)
          precond_violations <= precond_violations + 1'b1;
      end else begin
        if (occ_left != '0) occ_left <= occ_left - 1'b1;
        // Saturate rather than wrap: a wrapped interval would compute a
        // small gap from a long silence and hide a cadence miss.
        if (since_issue != {CAD_W{1'b1}}) since_issue <= since_issue + 1'b1;
      end

      // Obligation 2: no other activity during the occupancy window.
      if (occ_left != '0 && traffic_valid && occupancy_violations != 16'hFFFF)
        occupancy_violations <= occupancy_violations + 1'b1;

      // Obligation 3: the cadence. Counted once per crossing, not once
      // per cycle beyond it, so one long silence is one miss.
      if (ever_issued && since_issue == CADENCE[CAD_W-1:0]
          && cadence_misses != 16'hFFFF)
        cadence_misses <= cadence_misses + 1'b1;
    end
  end

  assign busy = (occ_left != '0);
endmodule

ever_issued gates the cadence check, and that ordering matters. Before the first issue there is no interval to be late against — so an unbooted system must not accumulate cadence misses, or the counter reports a failure that is really an absence.

The cadence miss is counted once per crossing rather than once per cycle beyond it. A silence of ten times the cadence is one miss of a known size, not ten thousand — and since_issue carries the size. Counting per cycle would produce a number whose magnitude reflects the sampling rate rather than the severity, which is 28.3 §3's overdue-versus-miss argument in a second domain.

And every check here passes on a device that ignored the command entirely. That is stated in the classification block because it is the honest limit: §10's shift is from verifying a result to verifying a contract, and a contract kept does not imply a result achieved.

11. The One Failure With No Digital Evidence

§1's fifth row, and the boundary this chapter has to hold as firmly as 28.1 §1 held the pass-bitmap boundary.

A calibration that started, converged, committed, read back correctly and remains valid can still produce a system that fails. The committed value is the best value the search could find, under the conditions it ran in, judged by its own classifier. None of those qualifiers is a margin measurement.

What the lifecycle establishesWhat it does not
A value was computed by a defined procedurethat the value is optimal
The value reached the hardwarethat the hardware behaves well with it
The conditions have not changed sincethat the conditions at calibration were representative
The classifier accepted itthat the classifier is discriminating — 28.1 §4

So a successful calibration is a precondition for correct operation, not evidence of it. The digital evidence is exhausted at valid, and everything beyond it is a data question — which is why 28.5 treats corruption as its own investigation rather than as a calibration outcome.

The practical consequence for an investigation is an ordering rule. When data corruption appears, check the four digital calibration failures first because they are cheap and definite, then stop. A clean lifecycle does not implicate the channel — it merely means calibration is no longer the cheapest hypothesis, and 28.6 owns what evidence would justify moving into the physical domain.

12. The Lifecycle Monitor

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ---------------------------------------------------------------------
// calib_lifecycle_monitor -- CLASSIFICATION: synthesisable, BINDABLE.
//
// WHAT IT DOES: publishes §1's four facts separately -- started,
// converged, committed, valid -- because one `done` flag makes five
// different failures indistinguishable.
//
// WHY VALID IS A COMPARISON, NOT A LATCH (§5): a latched valid bit
// asserts that nothing has changed since the commit, which is exactly
// the assumption staleness violates. Comparing a stored PROVENANCE TAG
// against the live conditions answers the question at any moment --
// including after a change nobody wired an invalidation for. A missed
// invalidation leaves a latched bit set; a tag comparison still
// returns false.
//
// WHY started IS A COUNT (§2, §7): "did it run" and "how many times"
// are different questions, and the retry investigation needs the
// second. A routine whose `done` is implemented as !busy reads done
// from the first cycle after reset, and start_count == 0 is the only
// evidence that distinguishes that from a real success.
//
// WHY THE READBACK (§3): a computed value and an applied value are
// different things, and five plumbing failures live between them.
//
// WHAT IT CANNOT TELL YOU: whether the value has margin (§11).
//
// SYNTHESIS: counters, a tag register, one comparator. No DUT handle.
// ---------------------------------------------------------------------
module calib_lifecycle_monitor #(
  parameter int CODE_W = 8,
  parameter int TAG_W  = 16,   // packed conditions the result depends on
  parameter int LANES  = 8
)(
  input  logic                     clk,
  input  logic                     rst_n,

  // ---- routine interface (observed only)
  input  logic                     precond_met,
  input  logic                     start,
  input  logic                     probing,
  input  logic                     converged,
  input  logic                     commit,
  input  logic [CODE_W-1:0]        computed_code,
  input  logic [CODE_W-1:0]        applied_code,   // read back from the datapath
  input  logic [$clog2(LANES)-1:0] lane,
  input  logic                     timeout,

  // ---- §5: the conditions the result depends on, packed by the caller
  input  logic [TAG_W-1:0]         conditions_now,

  input  logic                     clear,

  // ---- published evidence: four SEPARATE facts
  output logic [15:0]              start_count,
  output logic [15:0]              converge_count,
  output logic [15:0]              commit_count,
  output logic [15:0]              timeout_count,
  output logic                     ever_started,
  output logic                     have_result,
  output logic [CODE_W-1:0]        committed_code,
  output logic [TAG_W-1:0]         committed_tag,
  output logic                     valid,
  output logic                     readback_mismatch,
  output logic [CODE_W-1:0]        mismatch_computed,
  output logic [CODE_W-1:0]        mismatch_applied,
  output logic [$clog2(LANES)-1:0] mismatch_lane,
  output logic [15:0]              stale_events,
  output logic [CODE_W-1:0]        code_min,
  output logic [CODE_W-1:0]        code_max
);
  // $clog2(1) is 0, making the lane vector illegal as [-1:0].
  initial begin
    if (LANES  < 2) $fatal(1, "calib_lifecycle_monitor: LANES must be >= 2 (got %0d)", LANES);
    if (CODE_W < 1) $fatal(1, "calib_lifecycle_monitor: CODE_W must be >= 1");
    if (TAG_W  < 1) $fatal(1, "calib_lifecycle_monitor: TAG_W must be >= 1");
  end

  logic valid_q;

  always_ff @(posedge clk) begin
    if (!rst_n || clear) begin
      start_count       <= '0;
      converge_count    <= '0;
      commit_count      <= '0;
      timeout_count     <= '0;
      ever_started      <= 1'b0;
      have_result       <= 1'b0;
      committed_code    <= '0;
      committed_tag     <= '0;
      readback_mismatch <= 1'b0;
      mismatch_computed <= '0;
      mismatch_applied  <= '0;
      mismatch_lane     <= '0;
      stale_events      <= '0;
      code_min          <= {CODE_W{1'b1}};
      code_max          <= '0;
      valid_q           <= 1'b0;
    end else begin
      // §2: a count, not a flag. Saturating -- a wrapped start count
      // reading 0 would reproduce the exact failure it detects.
      if (start) begin
        ever_started <= 1'b1;
        if (start_count != 16'hFFFF) start_count <= start_count + 1'b1;
      end
      if (converged && converge_count != 16'hFFFF) converge_count <= converge_count + 1'b1;
      if (timeout   && timeout_count  != 16'hFFFF) timeout_count  <= timeout_count  + 1'b1;

      if (commit) begin
        if (commit_count != 16'hFFFF) commit_count <= commit_count + 1'b1;
        committed_code <= computed_code;
        // §5: capture the conditions the value was calibrated AGAINST.
        // This is what makes validity computable later.
        committed_tag  <= conditions_now;
        have_result    <= 1'b1;

        // §7 row three: the spread of committed values across boots is
        // the honest proxy for how close the decision is -- in CODE
        // STEPS, which 21.4 is explicit are not volts.
        if (computed_code < code_min) code_min <= computed_code;
        if (computed_code > code_max) code_max <= computed_code;

        // §3: the readback test. Latched first-wins: a later matching
        // commit must not erase the evidence of an earlier mismatch.
        if (!readback_mismatch && (applied_code !== computed_code)) begin
          readback_mismatch <= 1'b1;
          mismatch_computed <= computed_code;
          mismatch_applied  <= applied_code;
          mismatch_lane     <= lane;
        end
      end

      // §5: count the transitions of validity from true to false, so a
      // staleness event is a recorded fact rather than a momentary
      // level nobody sampled.
      valid_q <= valid;
      if (valid_q && !valid && stale_events != 16'hFFFF)
        stale_events <= stale_events + 1'b1;
    end
  end

  // §5's central decision: validity is a COMPARISON, evaluated now.
  // A result is valid when one exists, its commit was clean, and the
  // conditions it was calibrated against still hold.
  always_comb begin
    valid = have_result && !readback_mismatch
         && (committed_tag == conditions_now);
  end
endmodule

valid is combinational and that is the design's whole argument. A latched validity bit is a claim that nothing has changed; a comparison is a statement about the present, and it stays correct for configuration changes nobody remembered to wire an invalidation for.

readback_mismatch latches first-wins. §3's failures are often intermittent — a commit that races the datapath succeeds most of the time — and a flag that cleared on the next good commit would erase the only evidence of a race.

And code_min/code_max implement §7's spread. Across repeated boots they bound how much the committed value moves under nominally identical conditions. A spread of zero means the routine is deterministic; a wide spread means the decision depends on something uncontrolled — and the units are code steps, which 21.4 establishes are not volts.

13. What the Assertions Prove

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// Bound to §12's monitor. Every property carries `disable iff`, and
// every antecedent is covered below -- 27.2 measured this curriculum
// at 78.53% implications, so a silent pass is otherwise
// indistinguishable from an unbound block.
module calib_lifecycle_sva #(
  parameter int CODE_W = 8, parameter int TAG_W = 16
)(
  input logic clk, rst_n, clear,
  input logic precond_met, start, probing, converged, commit, timeout,
  input logic [CODE_W-1:0] computed_code, applied_code, committed_code,
  input logic [TAG_W-1:0]  conditions_now, committed_tag,
  input logic valid, have_result, ever_started, readback_mismatch,
  input logic [15:0] start_count, converge_count, commit_count, stale_events
);
  // ---- P1. FORBIDDEN. Validity is never asserted without a result.
  // §2: a routine that never ran must not read as usable.
  property p_no_valid_without_result;
    @(posedge clk) disable iff (!rst_n)
      valid |-> have_result;
  endproperty
  assert property (p_no_valid_without_result)
    else $error("calib: valid asserted with no committed result");

  // ---- P2. FORBIDDEN. Validity is never asserted when the conditions
  // differ from the ones the value was calibrated against. §5 -- this
  // is the staleness contract, and it holds WITHOUT any invalidation
  // signal having been wired.
  property p_valid_implies_tag_match;
    @(posedge clk) disable iff (!rst_n)
      valid |-> (committed_tag == conditions_now);
  endproperty
  assert property (p_valid_implies_tag_match)
    else $error("calib: valid with a stale provenance tag");

  // ---- P3. FORBIDDEN. A readback mismatch permanently denies
  // validity. §3: the computed value never reached the hardware.
  property p_mismatch_denies_valid;
    @(posedge clk) disable iff (!rst_n)
      readback_mismatch |-> !valid;
  endproperty
  assert property (p_mismatch_denies_valid)
    else $error("calib: valid asserted despite a readback mismatch");

  // ---- P4. A condition change drops validity in the SAME cycle.
  // §6's defect is a bit that did not fall; this is the property that
  // would have caught it.
  property p_condition_change_drops_valid;
    @(posedge clk) disable iff (!rst_n)
      (have_result && (conditions_now != committed_tag)) |-> !valid;
  endproperty
  assert property (p_condition_change_drops_valid)
    else $error("calib: conditions changed and valid stayed asserted");

  // ---- P5. FORBIDDEN. The readback mismatch flag is sticky. §12: an
  // intermittent commit race must not be erased by the next good one.
  property p_mismatch_sticky;
    @(posedge clk) disable iff (!rst_n)
      (readback_mismatch && !clear) |=> readback_mismatch;
  endproperty
  assert property (p_mismatch_sticky)
    else $error("calib: a readback mismatch was cleared by a later commit");

  // ---- P6. ever_started is sticky and set by a start. §2: it is the
  // one bit separating "never ran" from "ran and succeeded".
  property p_ever_started_faithful;
    @(posedge clk) disable iff (!rst_n)
      start |=> ever_started;
  endproperty
  assert property (p_ever_started_faithful)
    else $error("calib: a start did not set ever_started");

  property p_ever_started_sticky;
    @(posedge clk) disable iff (!rst_n)
      (ever_started && !clear) |=> ever_started;
  endproperty
  assert property (p_ever_started_sticky)
    else $error("calib: ever_started dropped");

  // ---- P7. FORBIDDEN. A commit never occurs while the search is
  // still probing. Committing mid-search applies a value the search
  // has not finished evaluating.
  property p_no_commit_while_probing;
    @(posedge clk) disable iff (!rst_n)
      commit |-> !probing;
  endproperty
  assert property (p_no_commit_while_probing)
    else $error("calib: commit while the search was still probing");

  // ---- P8. FORBIDDEN. A start requires its precondition. §2: a
  // routine started without its lifecycle precondition produces a
  // result computed under undefined conditions.
  property p_start_requires_precondition;
    @(posedge clk) disable iff (!rst_n)
      start |-> precond_met;
  endproperty
  assert property (p_start_requires_precondition)
    else $error("calib: started without its precondition");

  // ---- P9. INVARIANT. Commits never exceed starts. More commits than
  // starts means something is committing outside the routine.
  property p_commits_le_starts;
    @(posedge clk) disable iff (!rst_n)
      (commit_count <= start_count);
  endproperty
  assert property (p_commits_le_starts)
    else $error("calib: %0d commits against %0d starts", commit_count, start_count);

  // ---- P10. INVARIANT. Convergences never exceed starts.
  property p_converges_le_starts;
    @(posedge clk) disable iff (!rst_n)
      (converge_count <= start_count);
  endproperty
  assert property (p_converges_le_starts)
    else $error("calib: more convergences than starts");

  // ---- P11. FORBIDDEN. A timeout and a convergence never coincide.
  // §1: they are different exits and a routine reporting both has
  // collapsed the distinction the whole chapter depends on.
  property p_timeout_xor_converge;
    @(posedge clk) disable iff (!rst_n)
      !(timeout && converged);
  endproperty
  assert property (p_timeout_xor_converge)
    else $error("calib: timeout and converged asserted together");

  // ---- P12. The committed code is the computed code at commit. §3's
  // transform failures are between COMPUTED and APPLIED, not here --
  // this property pins the monitor's own capture so a mismatch cannot
  // be blamed on the recorder.
  property p_capture_is_faithful;
    @(posedge clk) disable iff (!rst_n)
      commit |=> (committed_code == $past(computed_code));
  endproperty
  assert property (p_capture_is_faithful)
    else $error("calib: the monitor did not capture the computed code");

  // ---- 27.2 §7: publish every antecedent.
  cover property (@(posedge clk) disable iff (!rst_n) start);
  cover property (@(posedge clk) disable iff (!rst_n) probing);
  cover property (@(posedge clk) disable iff (!rst_n) converged);
  cover property (@(posedge clk) disable iff (!rst_n) commit);
  cover property (@(posedge clk) disable iff (!rst_n) timeout);
  cover property (@(posedge clk) disable iff (!rst_n) clear);
  cover property (@(posedge clk) disable iff (!rst_n) valid);
  cover property (@(posedge clk) disable iff (!rst_n) have_result && !valid);
  cover property (@(posedge clk) disable iff (!rst_n) readback_mismatch);
  cover property (@(posedge clk) disable iff (!rst_n) !ever_started);
  cover property (@(posedge clk) disable iff (!rst_n) stale_events != 16'd0);
  cover property (@(posedge clk) disable iff (!rst_n)
                  have_result && (conditions_now != committed_tag));
  cover property (@(posedge clk) disable iff (!rst_n) commit && !precond_met);
endmodule

14. DV — Testing the Lifecycle

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// SIMULATION-ONLY. Independent reference. It models validity by
// REPLAYING the event history and asking whether a condition change
// has occurred since the last commit, rather than by comparing a
// stored tag. A different formulation, so agreement is evidence --
// 27.4 §15's argument.
class calib_reference;
  typedef struct { int code; int tag; bit clean; } result_t;
  result_t history[$];
  int current_conditions;

  function void commit(int code, int tag, bit readback_ok);
    result_t r; r.code = code; r.tag = tag; r.clean = readback_ok;
    history.push_back(r);
  endfunction

  function void set_conditions(int c); current_conditions = c; endfunction

  // Validity by replay: the last commit must exist, have been clean,
  // and have been taken under the conditions in force now.
  function bit valid();
    if (history.size() == 0) return 1'b0;
    if (!history[$].clean)   return 1'b0;
    return history[$].tag == current_conditions;
  endfunction

  // §7 row three: the spread of committed codes across repeats, in
  // CODE STEPS -- which 21.4 establishes are not volts.
  function int code_spread();
    int lo, hi;
    if (history.size() == 0) return -1;      // not zero: no data
    lo = history[0].code; hi = history[0].code;
    foreach (history[i]) begin
      if (history[i].code < lo) lo = history[i].code;
      if (history[i].code > hi) hi = history[i].code;
    end
    return hi - lo;
  endfunction

  function int commits();  return history.size(); endfunction

  // §3: the count of commits whose readback did not match.
  function int dirty_commits();
    int n = 0;
    foreach (history[i]) if (!history[i].clean) n++;
    return n;
  endfunction
endclass
CheckWhat it establishes
Replay §6's trace into both; compare valid every cycleTag comparison and replay model agree, including at cycle 8
20,000 random commit/condition-change sequencesTwo formulations of validity agree
No commit ever; assert validFalse — P1
Commit, then change conditionsvalid falls with no invalidation wired — P2, P4
Commit with applied != computedreadback_mismatch sets; valid false — P3
A later clean commit after a mismatchMismatch stays set — P5
start with precond_met lowP8 fires
commit while probing highP7 fires
timeout and converged togetherP11 fires
Inject more commits than startsP9 fires
Boot 50 times, record committed codescode_max − code_min is the spread; reference agrees
code_spread() with no historyReturns −1, not 0
Tie start low; read done from a !busy sourceever_started false — the §2 signature
Run with the routine disconnectedAll 12 properties pass; all 13 covers empty

Two runs are worth publishing, and the first is the one this chapter exists to make visible:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  TWO PASSING CALIBRATION REPORTS

  (A) calibration never ran
        stimulus : the precondition is never satisfied; the platform's
                   `done` is wired to !busy
        done (platform view)        1      from the first cycle
        start_count                 0
        ever_started                0      <-- the only honest field
        converge_count              0
        commit_count                0
        have_result                 0
        valid                       0
        ------------------------------------------------
        all 12 properties           PASS
        all 13 covers               0 hits

        every property passes because nothing happened. The platform
        reports calibration complete. 28.1's hypothesis C reads this
        prerequisite's flag as TRUE and proceeds -- and the training
        failure that follows is investigated as a training problem for
        the rest of the afternoon.

        ONE field distinguishes it: ever_started.

  (B) calibration succeeded and the system corrupts data
        stimulus : full lifecycle, conditions held, traffic running
        start_count                 1
        converge_count              1
        commit_count                1
        readback_mismatch           0
        valid                       1
        stale_events                0
        code_min / code_max         42 / 42    (spread 0 over 50 boots)
        ------------------------------------------------
        all 12 properties           PASS
        all 13 covers               HIT
        data errors                 present

        the routine started, converged, committed, read back clean and
        remains valid. The committed code is perfectly repeatable
        across fifty boots. And data is corrupt.

        this is §11: the digital evidence is EXHAUSTED at valid. Four
        of five failures are excluded and the fifth has no digital
        signature at all.

    diagnosis : (A) is the vacuity case with a calibration face -- and
      it is the one that costs the most debugging time, because it
      sends the investigation to the WRONG CHAPTER. (B) is honest
      exhaustion: calibration is no longer the cheapest hypothesis,
      which is a result, not a failure of the instrument.

    the fix : (A) publish ever_started and never implement done as
      !busy. (B) hand off to 28.5 with the lifecycle evidence
      attached, so the corruption investigation starts knowing the
      four digital calibration failures are excluded.

Report (B)'s last line is the right way to close a calibration investigation. A clean lifecycle is a negative result worth carrying forward — it tells the next investigation which four hypotheses are already excluded, which is exactly what 28.7's staged order is built to accumulate.

15. Corner Cases

CaseBehaviourWhy
LANES = 1Elaboration fails$clog2(1) is 0; the lane vector would be [-1:0]
Routine never starts, done is !busyever_started = 0§2 — the only distinguishing field
Commit with applied != computedreadback_mismatch sets, valid false§3, P3
A clean commit after a mismatchMismatch stays setP5 — an intermittent race must not be erased
Conditions change after commitvalid falls with no invalidation wired§5 — the tag comparison
Invalidation signal forgotten entirelyStill falls§5 — the argument for tags
A tag missing a fieldvalid stays true incorrectly§5 — visible by inspection, not by any property
commit while probingP7 firesthe search had not finished
start without preconditionP8 fires§2
timeout and converged togetherP11 fires§1 — different exits
50 boots, spread 0Deterministic routine§7 row three
50 boots, spread 6 code stepsResult depends on something uncontrolled§7 — and steps are not volts
ZQ-style unverifiable resultP3, P4, P12 have nothing to constrain§10 — verify obligations instead
Reads clean, writes corruptDirection-specific setting§8 — a strong, rare signature
Everything valid, data corruptNo digital evidence remains§11 — hand off to 28.5

Rows seven and fifteen are the two honest limits. A tag that omits a field and a committed value with no margin both pass every property in this chapter, and neither is detectable by adding another assertion.

16. Misconceptions

“Calibration reported done, so it worked.” §1. Five failures share that flag, and one of them is a routine that never started.

done means the routine finished.” §2. Implemented as !busy, it reads done from the first cycle after reset. Publish a start count.

“Converged means committed.” §3. Five plumbing failures live between the computed value and the applied one, and only a readback finds them.

“A valid flag means the value still applies.” §5. A latched bit asserts nothing has changed. A tag comparison checks it, including for changes nobody wired an invalidation for.

“We invalidate on every configuration change.” §5. That is a list someone had to remember. The change you forgot is the one that produces the bug.

“It failed once and passed on retry, so it is fine.” §7. The second pass ran in a state the first did not. Reset fully between passes, or the comparison is meaningless.

“The committed code is repeatable, so there is margin.” §7, §11. Repeatability says the routine is deterministic. It says nothing about how close the decision was.

“A wide spread across boots means the device is bad.” §7. It means the result depends on something uncontrolled — which may be temperature, supply settling, or leftover state.

“ZQ returned, so termination is calibrated.” §10. Chapter 7.7 establishes the result is not observable. What is checkable is the precondition, the occupancy and the cadence.

“Reads are clean, so the link is fine.” §8. Direction-asymmetric corruption is a strong signature of a direction-specific setting, and 21.4 establishes the two sides' ranges do not overlap where it matters.

“A clean calibration means the problem is the channel.” §11. It means calibration is no longer the cheapest hypothesis. Chapter 28.6 owns what would justify the physical domain.

17. Interview Reasoning

Calibration reports success. What do you actually know? That a flag is set. Whether the routine started, converged, committed, read back correctly, and still applies are four separate facts, and a single flag conflates them.

How can a routine that never ran report done? If done is !busy, it is true from reset. The distinguishing evidence is a start count of zero.

Why read the applied value back? Because a computed value and an applied value are different things. A gated write, a wrong target, an overwrite, or a byte-swap all produce a converged search and a wrong datapath.

How do you know a committed calibration still applies? Store what it was calibrated against and compare that tag with the live conditions. A latched valid bit asserts that nothing changed; a comparison checks it.

Why prefer a tag over invalidation signals? Invalidation is a list of events someone remembered to wire. A tag comparison catches the change nobody anticipated.

Calibration fails on the first boot and passes thereafter. What is your hypothesis? The first pass ran in a different state — cold device, unsettled supplies, or state a later pass inherits. Reset fully before the second pass; if it then fails, the state was causal.

Committed codes vary by six steps across identical boots. Is that a problem? It says the decision depends on something uncontrolled. It is not a margin measurement — code steps are not volts.

Reads are clean and writes corrupt. Where do you look? At direction-specific settings. Few mechanisms are direction-asymmetric, so it is strong evidence, and the reference levels for the two directions are not interchangeable.

How do you verify a calibration whose result you cannot read? You do not verify the result. You verify the obligations around it: that the precondition held when it issued, that its occupancy was respected, and that it issues at the required cadence.

Calibration is clean and data is corrupt. What have you learned? That four of the five calibration failures are excluded. That is a real negative result to carry into the corruption investigation, not a dead end.

18. Exercises

  1. §1 lists five failures behind one flag. Design the minimum set of published fields that distinguishes all five, and prove no smaller set works.

  2. §5 argues for tags over invalidation. Construct the configuration change most likely to be omitted from a tag, and say what symptom its omission produces.

  3. §3 lists five commit failures. For each, give an observation that implicates it uniquely, or argue that two of them are indistinguishable from the readback alone.

  4. §7 measures a spread in code steps. Using 21.4's code-step-versus-voltage-step distinction, explain what a spread of six steps does and does not bound.

  5. §10 shifts verification from the result to the obligations. Write the three checkable statements for an unverifiable calibration, and say which of §1's five failures each one can still catch.

  6. Report (A) sends the investigation to the wrong chapter. Trace the cost: list every step of 28.1's investigation that would run before ever_started was read.

  7. §12's valid is combinational. Argue for and against registering it, considering both a consumer that samples it and a designer debugging a glitch.

  8. A colleague proposes recalibrating automatically whenever data corruption is detected. Identify which of §1's five failures that would mask, and design the evidence you would capture before the recalibration runs.

19. Where This Goes

A calibration done flag is the weakest evidence in DDR bring-up. Five failures share it — never started, never converged, never committed, no longer valid, and valid but insufficient — and only the first four have digital signatures. Validity is a comparison against the conditions a value was calibrated against, not a latched bit. And for an unverifiable mechanism there is no result to check at all, so the obligations around the command replace claims about its outcome.

Four results carry forward. started must be a count, not a flag, because a routine that never ran reports done under the most natural implementation of done. The readback is the only check that separates a converged search from a working datapath. A provenance tag beats an invalidation list, because it catches the change nobody wired. And a clean lifecycle is a negative result worth carrying forward — four excluded hypotheses handed to the next investigation.

Two things stay open, and both are honest limits rather than gaps. A tag that omits a field passes every property in §13, and no assertion can detect a missing field — only inspection can. And the fifth failure has no digital evidence at all. §11 is explicit that the evidence is exhausted at valid.

Chapter 28.5 is where report (B) goes. It is the module's central chapter because it is where every other family's failures surface: a mis-committed calibration, a missed refresh deadline, a violated timing contract and a badly chosen training tap can all present as one wrong byte. The question there is not what caused it — it is what structure the error has, because an error's distribution across address, bank, lane, beat, pattern, frequency and temperature is the only thing that narrows a list of causes that otherwise includes this entire module.

Continue learning

Standards & specifications

Governing standard
JEDEC JESD79 (DDR SDRAM)(opens JEDEC Solid State Technology Association in a new tab)

Defines the DDR SDRAM device itself — signals, command encoding, mode registers, timing parameters and the initialisation sequence — one document per generation. Memory-controller microarchitecture, address-mapping policy, PHY training algorithms and board-level design are not specified by it.

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 DDR curriculum.