Skip to content
VLSI Mentor

DDR · Module 22

On-Die Termination

Chapter 6.7 decided which ranks terminate and declined the rest. This owns what it declined: three selectable values, the dynamic switch scoped to one rank, and a posted latency tied to write latency.

Chapter 6.7 built a termination policy and was unusually explicit about its own limits. Its RTL header says it models no resistance, no strength selection and no mode-register values, and its limitations section closes with a sentence that reads as a handover:

“No timing — when ODT should assert and deassert relative to the data window is a real and difficult scheduling problem this block does not touch.”

That chapter decided which ranks are told to terminate. This one owns what they terminate with, when the value changes, and how the change is timed — which turns out to be three selectable resistances, a switch between two of them during every write, and a latency expressed relative to write latency.

The module's central law, stated once and used throughout:

A DDR signal arrives degraded by effects that are electrical, not logical. Termination, encoding and protection do not remove those effects — they bound them, detect them, or trade one for another.

For this chapter the qualification is immediate: ODT does not eliminate reflections. It attenuates them, it costs static power whenever it is presented, and choosing a value is choosing a point on a trade rather than solving a problem. Chapter 22.2 owns what is actually being attenuated.

1. Three Resistances, Not One

The single most common mental model of ODT is a switch: termination on, termination off. That model is wrong in a specific and consequential way — a DDR4 device has three separately programmed termination values, and which one is presented depends on both a pin and what the device is currently doing.

ValueProgrammed inPresented when
RTT_PARKMR5The rank is idle — nothing asserted, no transfer
RTT_NOMMR1The ODT pin is asserted for this rank
RTT_WRMR2This rank is the target of a write, and dynamic ODT is enabled

Three values exist because the three situations have genuinely different requirements, and this is the argument worth carrying out of the chapter.

A rank receiving a write needs the strongest termination. Its receiver is trying to resolve a signal the controller drove down a shared channel. Reflected energy arriving at its pins directly attacks the decision. This is RTT_WR's job.

A rank not being accessed still affects the channel. Its pins are attached to the same net. Terminating there absorbs energy that would otherwise reflect back toward the receiver that matters — which is why 6.7's policy table has non-target ranks terminating at all. This is RTT_NOM's job, and it wants a different, usually weaker, value than the receiving rank.

An idle rank still has pins on the net. Leaving them entirely unterminated turns every idle device into an unmatched stub. RTT_PARK exists so the channel has a defined termination even when nothing is being addressed — and it is the value most easily forgotten, because nothing appears to be happening when it is in effect.

2. The Register Map

All three values are programmed independently, and the encodings are worth reading together because they are nearly identical — which is itself informative.

RTT_NOM — MR1 bits A10, A9, A8:

CodeValueCodeValue
000RTT_NOM Disable100RZQ/1
001RZQ/4101RZQ/5
010RZQ/2110RZQ/3
011RZQ/6111RZQ/7

RTT_PARK — MR5 bits A8, A7, A6: the same eight codes with the same meanings, with 000 being RTT_PARK Disable.

RTT_WR — MR2 bits A10, A9: a deliberately different and much smaller set.

CodeValue
00Dynamic ODT Off
01RZQ/2
10RZQ/1
11Hi-Z

Three observations, and each one carries engineering content.

RTT_NOM and RTT_PARK share an encoding. Eight codes, identical divisors, identical order. That is a strong hint that the two are the same physical resource selected by different conditions, and it means a firmware routine that programs one can share its table with the other.

RTT_WR has only four codes, and two of them are not resistances. 00 disables the whole dynamic scheme, and 11 selects Hi-Z — no termination at all during a write. A four-code field says the specification does not expect this value to be finely tuned; it expects one of a couple of strong options, off, or explicitly nothing.

Hi-Z as an option is not an oversight. There are topologies where the write-target rank should not terminate harder — where the additional loading costs more than the reflection it absorbs. RTT_WR = Hi-Z makes dynamic ODT's switch a switch to no termination, which is a real configuration and a genuinely surprising one.

The driver side is programmed separately and much more narrowly. Output Driver Impedance Control — MR1 bits A2, A1 offers only RZQ/7 and RZQ/5, with the other two codes reserved.

3. Why Divisors of a Calibrated Reference

It is worth a moment on why the encoding is RZQ/n rather than a number of ohms, because the reason connects two modules.

Chapter 7.7 established that ZQ calibration tunes the device's output drivers and termination against an external precision resistor, and that the result is not observable to the controller. The device ends up with an internally trimmed reference.

Expressing termination as RZQ/n means the mode register selects a ratio against that trimmed reference. The benefit is that process, voltage and temperature variation in the on-die resistance is absorbed by the calibration rather than appearing in the mode-register value — so the same code means approximately the same thing across parts and across temperature.

The cost is that no code in this chapter has a knowable absolute value from the controller's side. A controller programming RZQ/2 does not know what resistance it has selected; it knows it has selected half of whatever the device calibrated to. That is the same epistemic position Module 21 kept running into with delay codes and reference-level codes, appearing a third time.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   ZQ calibration (7.7)        mode register (this chapter)
   ─────────────────────        ───────────────────────────
   trims an internal            selects a RATIO against it
   reference against an           RTT_NOM  = RZQ/6
   external precision             RTT_WR   = RZQ/2
   resistor                       RTT_PARK = RZQ/1

   result NOT observable        code IS observable, and its
   to the controller            absolute value is NOT

4. Dynamic ODT — What Actually Switches

Dynamic ODT is the mechanism that gives a write-target rank a different termination from the one the ODT pin selected, for the duration of the write and no longer.

Without it, a rank has one termination value while the ODT pin is asserted. With it, the rank presents RTT_NOM when the pin is asserted and switches to RTT_WR while a write to that rank is in progress, then switches back.

The sequence over a write, in the device's terms:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   idle                RTT_PARK        nothing addressed

     │  ODT asserted for this rank

   terminating         RTT_NOM         pin-selected value

     │  write burst to THIS rank begins

   receiving a write   RTT_WR          the dynamic switch

     │  burst ends

   terminating         RTT_NOM         back to the pin value

     │  ODT released

   idle                RTT_PARK

Two things about that sequence are easy to get wrong.

The switch is not caused by the ODT pin. The pin took the device from RTT_PARK to RTT_NOM. The switch to RTT_WR is caused by the write itself, which the device knows about from the command stream, not from the pin. So dynamic ODT is a case where termination changes without the pin changing — and a controller that models termination as a function of the pin alone will mispredict the device's state.

It returns to RTT_NOM, not to RTT_PARK. The pin is still asserted after the burst, so the intermediate value is the pin's. Getting this wrong produces a model that drops termination entirely between back-to-back writes, which is precisely when it is most needed.

5. The Timing 6.7 Declined

Here is the problem 6.7 named and deliberately did not solve: termination must be correct when the data arrives, and the ODT pin is asserted some time earlier. The gap is specified.

JESD79-4 names the relevant quantities:

SymbolMeaning
DODTLonDirect ODT turn-on latency
DODTLoffDirect ODT turn-off latency
tADCRTT change timing skew
tAONASAsynchronous RTT_NOM turn-on delay
tAOFASAsynchronous RTT_NOM turn-off delay

And it gives the first two a value that is not a constant but a relationship:

DODTLon = WL − 3 and DODTLoff = WL − 3.

That is worth staring at. The ODT turn-on latency is expressed in terms of write latency — the same WL that Chapter 11.2 established as AL + CWL. Termination timing is tied to the write data pipeline, because the thing termination has to be ready for is the write data.

Three consequences follow directly.

Termination scheduling is the same scheduling problem as the write data window. A controller that already computes when write data will be on the bus has computed, up to a constant, when ODT must assert. That is why 6.7 said the decision “is computed alongside the data-window scheduling” — the two share their hard part.

The latency changes when WL changes. WL depends on CWL, which is a speed-bin-dependent mode-register setting, and on additive latency. So DODTLon is not a number a design can hard-code; it moves with configuration. A controller ported to a new speed grade with CWL updated and its ODT lead left alone will assert termination at the wrong time, and 6.7 §3 already described what that looks like: functionally correct, electrically marginal, nothing failing a test.

tADC is the cost of the dynamic switch. Changing RTT is not instantaneous, and tADC is the skew allowance for it. Every dynamic-ODT transition spends some of it, which is the honest reason dynamic ODT is not simply free.

6. Posted ODT — Why the Latency Is Not a Delay Line

JESD79-4 organises synchronous termination under a heading worth reading carefully: ODT Latency and Posted ODT. The word posted is doing real work, and mistaking what it means produces a controller that double-counts its own lead.

DODTLon is not a delay the controller inserts. It is the device's internal latency from registering the assertion to the termination actually being present at the pins. The controller's job is to assert early enough that the device's own pipeline delivers termination when the data arrives — which is why the quantity is expressed against WL rather than as an absolute time.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   WRONG MODEL -- the controller inserts the delay

     controller decides    ──┐
                             │  inserts WL-3 of its own delay

     asserts ODT           ──┐
                             │  device adds ITS latency too

     termination present     far too late

   CORRECT MODEL -- the latency is the device's

     controller decides and asserts

             │  device's internal DODTLon = WL - 3

     termination present  ──  aligned with the write data

So the controller schedules the assertion against the data window, and the device's posted latency is what closes the gap. A design that treats DODTLon as a budget it must itself provide will assert 2 × (WL − 3) early, and the termination will be present long before the data and released long before the burst ends.

Two consequences follow, and both show up in §20's debugging table.

The lead is a property of the device, not a tunable. There is no register that adjusts DODTLon. It moves only because WL moves, and WL moves because CWL or additive latency was reprogrammed. A controller cannot compensate for a mis-scheduled assertion by trimming the latency, because the latency is not its to trim.

Posted means clock-referenced, which is exactly what asynchronous mode is not. §7's three modes divide on this point. In synchronous and dynamic operation the device's response is pipelined against the clock, so a cycle count is a meaningful specification. In asynchronous mode there is no such pipeline, and the specification switches to the tAONAS and tAOFAS delays instead — a different kind of quantity, for a state in which the clock cannot be relied on.

7. Three Modes, and Why There Are Three

JESD79-4 organises on-die termination into three modes, and the division is not arbitrary.

Synchronous ODT mode. The device's termination changes are timed to the clock, using DODTLon and DODTLoff and the posted-ODT concept. This is the normal operating mode and it is what §5 describes.

Dynamic ODT. The RTT_NOM-to-RTT_WR switch of §4, layered on top of synchronous mode.

Asynchronous ODT mode. Termination changes are not clock-timed, and the delays are the tAONAS and tAOFAS above. This mode exists because there are device states in which the clock cannot be relied on for this purpose — and the specification also defines an ODT buffer disabled mode for power-down, controlled by the MR5 bit mentioned in §2.

The existence of an asynchronous mode is the interesting part. Termination is one of the few functions a DDR device performs that cannot simply stop when the clock does, because the channel is still physically present. A device in a low-power state with its clock stopped still has pins on the net, and whatever termination it is presenting is still loading that net.

That is why MR5 has a bit for whether the ODT input buffer is even active during power-down, and why the standard notes it is ignored when RTT_NOM is disabled — if the pin does nothing, whether its buffer is powered is moot.

8. The Pin Can Be Made Irrelevant

One detail from §2's verified set deserves its own section because it inverts a common assumption.

The ODT pin is ignored if MR1 is programmed to disable RTT_NOM.

So the relationship between the pin and termination is not “pin asserts, device terminates.” It is:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   RTT_NOM disabled (MR1 A10:A8 = 000)
       ODT pin has NO effect. The device presents RTT_PARK
       when parked, and RTT_WR during writes if dynamic ODT
       is enabled -- but the pin itself does nothing.

   RTT_NOM enabled
       ODT pin selects between RTT_PARK and RTT_NOM.

There is a matching initialisation requirement: if RTT_NOM is to be enabled in MR1, the ODT input signal must be statically held LOW during the power-up sequence, and the ODT input must remain static until initialisation finishes — including the expiry of tDLLK and tZQinit.

The practical lesson is a sequencing one. Termination configuration is not a single write. It is a set of register fields whose interactions decide whether a pin matters at all, plus a pin-state requirement during a window before any of those registers has been programmed. A bring-up sequence that programs MR1 and then asserts ODT has done it in the wrong order.

9. The Other End of the Channel

Everything so far has been about termination inside the DRAM. That is what the DDR4 specification means by on-die termination, and it is only half of the channel's story.

There are two receivers on a DDR data net, and they are active in opposite directions:

DirectionWho receivesWhose termination matters at the receiver
WriteThe DRAMThe DRAM's RTT_WR — this chapter's subject
ReadThe controllerThe PHY's input termination — not in this specification at all

On a read, the DRAM's RTT_NOM and RTT_PARK settings still matter, but not because they help the DRAM — the target DRAM is driving and must not terminate at all (6.7's invariant). They matter because the non-target ranks absorb energy that would otherwise reflect back toward the controller's receiver. That is the entire reason 6.7 §5's inversion exists, and §10's read table is where it appears.

This is why JESD79-4 gives ODT during Reads its own section. The question is not obvious: the device being read terminates nothing, yet termination on the channel is still being managed, on behalf of a receiver the specification does not describe.

The practical consequence is a debugging asymmetry. A write-direction termination problem is diagnosable against published register values and readback (§8). A read-direction termination problem is diagnosable only against the PHY vendor's documentation, and the symptom — marginal reads, no functional failure — looks identical.

10. The Multi-Rank Problem

Chapter 6.7 decided per-rank assertion from the access's direction and target. With three values and a dynamic switch, the multi-rank picture gains a dimension: not just which ranks terminate, but which value each one presents, and those are not independent.

For a write to rank 0 on a two-rank channel, with dynamic ODT enabled:

RankODT pinBeing writtenValue presentedWhy
0assertedyesRTT_WRThe receiver — dynamic switch applies
1assertednoRTT_NOMHelps the channel; not the receiver
1not assertednoRTT_PARKStill on the net, still loading it

And for a read from rank 0:

RankODT pinDrivingValue presentedWhy
0must not be assertedyesRTT_PARK6.7's invariant — a driver must not terminate itself
1assertednoRTT_NOMAbsorbs energy for the controller's receiver

The read row is 6.7 §5's inversion, and it is not re-argued here. What is new is the last column of the first table: the value the non-target rank presents is different from the value the target presents, and both are different from idle. A single odt bit per rank cannot express that. The state to be tracked is per rank and has more than two values.

Dynamic ODT's scope is the subtlety. The switch to RTT_WR applies to the rank being written. A non-target rank with its ODT asserted during that same write stays at RTT_NOM — it does not follow the target into RTT_WR. A model that applies the dynamic switch channel-wide over-terminates every idle rank on every write, which costs power continuously and changes the channel in a way nobody intended.

11. A Four-Rank Walk, With a Turnaround

The two-rank tables of §10 do not exercise the hard case. Here is a four-rank channel through a write to rank 1 followed immediately by a read from rank 3 — the direction turnaround, which is where termination scheduling is tightest.

Assume RTT_PARK enabled on all ranks, RTT_NOM enabled, dynamic ODT enabled, and the controller's policy from 6.7 asserting ODT on the write target and its neighbours.

PhaseR0R1R2R3Reasoning
IdlePARKPARKPARKPARKNothing addressed; every rank still loads the net (§1)
ODT asserted, write pendingNOMNOMNOMPARKPin selected; rank 1's window has not opened yet
Write burst to R1NOMWRNOMPARKDynamic switch, target only (§10)
Burst done, pin heldNOMNOMNOMPARKWindow closed; returns to NOM, not PARK (§4)
TurnaroundPARKPARKPARKPARKPin released; nothing driving yet
Read burst from R3NOMNOMNOMPARKR3 drives — the invariant; others absorb (§9)
IdlePARKPARKPARKPARK

Three rows repay attention.

The write-burst row is the dynamic scope. Exactly one cell changed from the row above it. A model that switched the whole channel would show WR in all three asserted columns, which over-terminates three ranks for the duration of every write — and §12 is about what that costs.

The read-burst row is the inversion, now with four ranks. R3 is the accessed rank and it is the only one at park. The three ranks not involved in the transfer are the ones terminating. This is 6.7 §5's counter-intuitive result, and at four ranks it looks even stranger: three quarters of the channel terminates so that the one rank doing the work does not have to.

The turnaround row is the scheduling constraint. Between the write and the read, termination must be released on the ranks that were terminating for the write and re-established for the read — and the two requirements are different. The window in which this happens is bounded by the bus turnaround the controller already has to schedule (17.1 owns the commit ordering; 12.4 owns the turnaround cost).

What makes it tight is §6. Both the release and the re-establishment are posted by WL − 3. The controller cannot assert termination for the read and have it appear instantly; it has to have decided, and asserted, that many cycles earlier — while the write's data was still on the bus. Termination for the read is scheduled during the write.

That is the single hardest thing about ODT scheduling, and it is the reason 6.7 called it a real and difficult problem rather than solving it in passing.

12. What Termination Costs

Termination is not free, and being precise about the cost is what makes the trade legible.

Static power, whenever it is presented. RTT is a resistance to a supply rail. Whenever a terminated pin is held at a level away from that rail, current flows continuously — not per transition, but for as long as the termination is presented. A stronger termination is a smaller resistance and therefore a larger current. This is why RTT_PARK deserves thought rather than a default: it is presented most of the time (§1), across every idle rank, on every pin.

Loading on the driver. The device driving the net sees every presented termination as a load. Terminating more ranks, or terminating them more strongly, means the driver must develop its signal into a heavier load — which reduces the swing at the receiver. So termination both helps (by absorbing reflections) and hurts (by reducing amplitude), and the optimum is a balance rather than a maximum.

tADC per switch. §5 named it: every change of RTT has a specified settling skew. Dynamic ODT introduces two extra switches per write — into RTT_WR and back — so the scheme's benefit is paid for partly in settling allowance.

The two blocks in §14 and §16 publish counters for exactly these: cnt_switches counts tADC spends, and n_terminating counts how many ranks are presenting a non-idle value at any moment.

The shape of the trade, stated without numbers because numbers would be invented:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   weaker termination                    stronger termination
   ──────────────────                    ────────────────────
   less static power                     more static power
   lighter load, larger swing            heavier load, smaller swing
   absorbs less reflected energy         absorbs more
   ──────────────────────────────────────────────────────────────
   the optimum is interior, and it is topology-dependent --
   which is why JEDEC provides eight codes and no recommendation

That last line is the argument for why §2's table has eight values and the specification declines to tell you which to use. The right value is a property of a board, and the standard specifies devices.

13. The Termination Architecture

The DDR4 termination architecture, arranged in four rows. The top row holds the controller, the on-die-termination scheduler that decides when assertion must happen, the mode registers that hold the three termination values, and ZQ calibration from chapter seven point seven which trims the internal reference. The second row holds the ODT assertion itself as seen at the device, the termination selector inside the device, and the RZQ reference that ZQ calibration established. The third row holds the three selectable values: RTT_PARK presented when the rank is idle, RTT_NOM presented when the ODT pin is asserted, and RTT_WR presented during a write to this rank when dynamic ODT is enabled. The bottom row is the data and strobe pins at the device, where whichever value is selected is actually applied. Edges run from the controller through the scheduler to the ODT assertion and into the selector, from the mode registers into the selector, from ZQ calibration through the RZQ reference into the selector, from the selector to each of the three values, and from each value to the device pins.Controllerknows the write windowODT schedulerDODTLon = WL − 3Mode registersMR1 / MR2 / MR5ZQ calibration7.7 — trims RZQODT assertedper rank — 6.7RTT selectorinside the deviceRZQ referenceabsolute value unknownRTT_PARKidle — the defaultRTT_NOMpin assertedRTT_WRwrite to THIS rankDQ / DQS pinswhere it is applied12

The node marked RZQ reference is the one to read carefully. Everything above it is digital and knowable; everything the selector chooses is a ratio against a value the controller never learns (§3). The diagram's three value nodes are selections, not resistances — and the RTL below outputs exactly that distinction.

14. The RTT State Sequencer

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ---------------------------------------------------------------------
// rtt_state_sequencer -- which termination value a DDR4 rank presents,
// and when it changes.
//
// CLASSIFICATION: educational, synthesisable. Owns the timing that
// Chapter 6.7's odt_policy explicitly declined ("no timing -- when ODT
// should assert and deassert relative to the data window is a real and
// difficult scheduling problem this block does not touch").
//
// WHAT IT DOES NOT MODEL:
//   - any resistance, impedance, ohm, or voltage. The output is an
//     ENUMERATION naming a selected configuration, not a value.
//   - RZQ, ZQ calibration, or the absolute value any code maps to
//     (7.7 owns the reference; §3 owns why it is unknowable here)
//   - tADC's actual skew during a switch, the analog switch itself,
//     reflections, or any notion of signal quality
//   - the asynchronous ODT mode of §7, which is by definition not
//     clock-timed and therefore not this block's shape
//
// SELECTING A TERMINATION IS NOT TERMINATING. Everything electrical is
// on the far side of the rtt_sel output.
// ---------------------------------------------------------------------
module rtt_state_sequencer #(
  // Write latency, AL + CWL (Chapter 11.2). DODTLon and DODTLoff are
  // both WL-3 per JESD79-4, so this parameter sets the ODT timing.
  parameter int WL = 12,
  // Beats of a write burst, in controller cycles. The dynamic-ODT
  // window lasts this long once it opens.
  parameter int WR_CYCLES = 4,
  // Configuration, mirroring the mode-register fields of §2. These are
  // ENABLES, not values -- this block never holds a resistance.
  parameter bit RTT_NOM_EN  = 1'b1,   // MR1 A10:A8 != 000
  parameter bit RTT_PARK_EN = 1'b1,   // MR5 A8:A6  != 000
  parameter bit DYN_ODT_EN  = 1'b1,   // MR2 A10:A9 != 00
  parameter int DODTL = WL - 3,
  parameter int CNT_W = (DODTL   <= 1) ? 1 : $clog2(DODTL + 1),
  parameter int WRC_W = (WR_CYCLES <= 1) ? 1 : $clog2(WR_CYCLES + 1)
) (
  input  logic             clk,
  input  logic             rst_n,

  // ── From Chapter 6.7's policy, for THIS rank. One bit: should this
  //    rank be terminating at the pin-selected value.
  input  logic             odt_assert,

  // ── From the controller's write pipeline. Asserted for one cycle
  //    when a write to THIS rank is committed. The dynamic-ODT window
  //    is scheduled from here, not from odt_assert -- §4's point that
  //    the switch is caused by the WRITE, not by the pin.
  input  logic             wr_committed_this_rank,

  // ── Asserted for one cycle when a READ from this rank is committed.
  //    Used only to check 6.7's invariant, never to select a value.
  input  logic             rd_committed_this_rank,

  // ── THE OUTPUT: which value is selected. An enumeration.
  //    0 = OFF (nothing selected -- park disabled and pin idle)
  //    1 = PARK, 2 = NOM, 3 = WR
  output logic [1:0]       rtt_sel,

  // ── Observability.
  output logic             dyn_window_open,
  output logic [CNT_W-1:0] odt_countdown,
  output logic             switching,        // a change this cycle
  output logic [15:0]      cnt_switches,     // tADC is spent per switch

  // 6.7's invariant, checked here because this block knows the value
  // and not just the bit: a driving rank must not be terminating at
  // anything other than its parked value.
  output logic             err_terminate_while_driving,
  output logic             err_dyn_without_nom,
  output logic             err_overlapping_write
);

  // ── Elaboration guards. §5's callout: WL-3 underflows for small WL,
  //    and an unsigned underflow becomes a huge latency rather than an
  //    obvious error.
  initial begin
    if (WL < 3)
      $fatal(1, "rtt_state_sequencer: WL (%0d) < 3 makes DODTL negative", WL);
    if (DODTL < 0)
      $fatal(1, "rtt_state_sequencer: DODTL (%0d) is negative", DODTL);
    if (WR_CYCLES < 1)
      $fatal(1, "rtt_state_sequencer: WR_CYCLES must be at least 1");
    // Dynamic ODT switches AWAY FROM RTT_NOM. With RTT_NOM disabled
    // the ODT pin does nothing (§8), so a dynamic scheme layered on it
    // is a configuration whose intent cannot be honoured.
    if (DYN_ODT_EN && !RTT_NOM_EN)
      $fatal(1, "rtt_state_sequencer: DYN_ODT_EN with RTT_NOM_EN=0");
  end

  localparam logic [1:0] SEL_OFF  = 2'd0;
  localparam logic [1:0] SEL_PARK = 2'd1;
  localparam logic [1:0] SEL_NOM  = 2'd2;
  localparam logic [1:0] SEL_WR   = 2'd3;

  // ── ODT assertion, delayed by DODTLon. The device does not respond
  //    to the pin immediately; termination becomes effective DODTL
  //    cycles later, which is why the controller asserts early.
  logic                nom_effective;
  logic [CNT_W-1:0]    on_cnt;
  logic                odt_q;

  // ── The dynamic-ODT window, scheduled from the write commitment.
  logic                dyn_open;
  logic [CNT_W-1:0]    dyn_lead;      // cycles until the window opens
  logic                dyn_pending;
  logic [WRC_W-1:0]    dyn_left;      // cycles the window stays open

  logic                rd_active;
  logic [WRC_W-1:0]    rd_left;

  // Declared with the other state because the continuous assignments
  // below read them -- a variable must be declared above the assign
  // that reads it.
  logic [1:0]          sel_q;
  logic [15:0]         sw_cnt;

  // ── The selection. Priority is the whole of §4 and §10:
  //    WR beats NOM (the dynamic switch), NOM beats PARK (the pin),
  //    and PARK is what idle means when it is enabled at all.
  logic [1:0] sel_next;
  always_comb begin
    if (DYN_ODT_EN && dyn_open)            sel_next = SEL_WR;
    else if (RTT_NOM_EN && nom_effective)  sel_next = SEL_NOM;
    else if (RTT_PARK_EN)                  sel_next = SEL_PARK;
    else                                   sel_next = SEL_OFF;
  end

  always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
      odt_q         <= 1'b0;
      on_cnt        <= '0;
      nom_effective <= 1'b0;
      dyn_open      <= 1'b0;
      dyn_pending   <= 1'b0;
      dyn_lead      <= '0;
      dyn_left      <= '0;
      rd_active     <= 1'b0;
      rd_left       <= '0;
      // SAFE STATE: parked if park is enabled, otherwise nothing
      // selected. Never NOM or WR out of reset -- both imply a
      // transfer that has not happened.
      sel_q         <= RTT_PARK_EN ? SEL_PARK : SEL_OFF;
      sw_cnt        <= '0;
    end else begin
      odt_q <= odt_assert;

      // ── Pin-to-effective delay, both directions. DODTLon and
      //    DODTLoff are equal (WL-3), so one counter serves both and
      //    the symmetry is the specification's, not a simplification.
      if (odt_assert != odt_q) begin
        on_cnt <= CNT_W'(DODTL);
      end else if (on_cnt != '0) begin
        on_cnt <= on_cnt - CNT_W'(1);
        if (on_cnt == CNT_W'(1)) nom_effective <= odt_q;
      end
      // DODTL == 0 degenerates to immediate effect, which is legal and
      // must not fall through the counter path above.
      if ((odt_assert != odt_q) && (DODTL == 0)) nom_effective <= odt_assert;

      // ── The dynamic-ODT window. Scheduled from the write, leading
      //    the data by the same DODTL the pin uses, because the switch
      //    has to be effective when the data arrives (§5).
      if (wr_committed_this_rank) begin
        dyn_pending <= 1'b1;
        dyn_lead    <= CNT_W'(DODTL);
      end else if (dyn_pending) begin
        if (dyn_lead != '0) begin
          dyn_lead <= dyn_lead - CNT_W'(1);
        end else begin
          dyn_pending <= 1'b0;
          dyn_open    <= 1'b1;
          dyn_left    <= WRC_W'(WR_CYCLES);
        end
      end

      if (dyn_open) begin
        if (dyn_left > WRC_W'(1)) begin
          dyn_left <= dyn_left - WRC_W'(1);
        end else begin
          // Window closes. §4: the device returns to RTT_NOM, NOT to
          // RTT_PARK -- the pin is still asserted. sel_next handles
          // that automatically, which is why the priority order
          // above is the whole mechanism.
          dyn_open <= 1'b0;
          dyn_left <= '0;
        end
      end

      // ── Read tracking, for the invariant check only.
      if (rd_committed_this_rank) begin
        rd_active <= 1'b1;
        rd_left   <= WRC_W'(WR_CYCLES);
      end else if (rd_active) begin
        if (rd_left > WRC_W'(1)) rd_left <= rd_left - WRC_W'(1);
        else begin rd_active <= 1'b0; rd_left <= '0; end
      end

      // ── Commit the selection, and count changes. Every change
      //    spends tADC (§5), so the count is a real cost metric and
      //    not just telemetry.
      sel_q <= sel_next;
      if ((sel_next != sel_q) && (sw_cnt != 16'hFFFF))
        sw_cnt <= sw_cnt + 16'd1;
    end
  end

  assign rtt_sel         = sel_q;
  assign dyn_window_open = dyn_open;
  assign odt_countdown   = on_cnt;
  assign switching       = (sel_next != sel_q);
  assign cnt_switches    = sw_cnt;

  // ── Chapter 6.7's invariant, now checkable against a VALUE rather
  //    than a bit: while this rank drives, it may present its parked
  //    value but never the pin's or the write's.
  assign err_terminate_while_driving =
           rd_active && ((sel_q == SEL_NOM) || (sel_q == SEL_WR));
  // A dynamic window with RTT_NOM disabled: §8 says the pin does
  // nothing, so the scheme has no value to switch away from.
  assign err_dyn_without_nom  = dyn_open && !RTT_NOM_EN;
  // A second write committed while a window is still open or pending.
  assign err_overlapping_write =
           wr_committed_this_rank && (dyn_open || dyn_pending);

endmodule

The priority chain in sel_next is the chapter compressed into four lines. RTT_WR over RTT_NOM over RTT_PARK over nothing is §4's sequence, and because the window closing simply removes the top priority, the return to RTT_NOM rather than RTT_PARK is structural rather than coded — there is no explicit “go back to NOM” anywhere, and that is why it cannot be got wrong.

15. A Dynamic ODT Write, Cycle by Cycle

RTT_PARK to RTT_NOM to RTT_WR and back

10 cycles
Ten cycles of a write to one rank with dynamic on-die termination enabled, using a deliberately reduced write latency so the whole sequence fits in ten cycles. The clock runs throughout. A write command is committed on the first cycle. The ODT signal for this rank asserts on the second cycle and stays asserted through the eighth. The termination selection begins at park, moves to the nominal value on the third cycle once the pin-to-effective latency has elapsed, then switches to the write value on the fifth cycle as the dynamic window opens, holds the write value for four cycles covering the burst, returns to the nominal value on the ninth cycle when the window closes while the pin is still asserted, and returns to park on the tenth once the pin has released. The data strobe and data valid rows show the burst occupying the same four cycles the write value covers, which is the entire point of the timing. The switch count row increments on each of the four selection changes, because each change spends the specified termination change skew.pin effective: PARK to NOMpin effective: PARK to NOMdynamic switch: NOM to WRdynamic switch: NOM to WRwindow closed: back to NOM, not PARKwindow closed: back to NOM,not PARKpin released: PARKpin released: PARKCKwr_commitodt_assertrtt_selPARKPARKNOMNOMWRWRWRWRNOMPARKdqsdq_validdyn_opencnt_switches0011222234t0t1t2t3t4t5t6t7t8t9

Two rows carry the lesson. dyn_open and dq_valid cover exactly the same four cycles — that alignment is what DODTLon = WL − 3 buys, and getting the lead wrong slides one relative to the other with no functional symptom.

And cycle 8 is the cycle most models get wrong. The window has closed, the burst is over, and the selection is NOM — not PARK — because the pin is still asserted. Only at cycle 9, after the pin releases, does the rank park.

Reduced latency for legibility. A real WL of 12 or more puts many more cycles between the command and the data; the sequence's shape is what this figure shows, not its true proportions.

16. The Multi-Rank Arbiter

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ---------------------------------------------------------------------
// multi_rank_odt_arbiter -- per-rank termination SELECTION across a
// multi-rank channel, with dynamic-ODT scope handled correctly.
//
// CLASSIFICATION: educational, synthesisable.
//
// WHAT IT DOES NOT MODEL:
//   - any resistance, impedance, or electrical quantity
//   - the channel, reflections, or whether a chosen combination is
//     good. §10: a valid selection per rank says nothing about the
//     combination's effect on the net.
//   - Chapter 6.7's odt_policy, which decides the per-rank ASSERTION
//     bit. This block consumes that decision and adds the VALUE.
//
// THE KEY BEHAVIOUR (§10): the dynamic switch to RTT_WR applies ONLY to
// the rank being written. A non-target rank with ODT asserted during
// that write stays at RTT_NOM. Applying the switch channel-wide
// over-terminates every idle rank on every write.
// ---------------------------------------------------------------------
module multi_rank_odt_arbiter #(
  parameter int NUM_RANKS = 4,
  parameter bit RTT_NOM_EN  = 1'b1,
  parameter bit RTT_PARK_EN = 1'b1,
  parameter bit DYN_ODT_EN  = 1'b1,
  parameter int RK_W = (NUM_RANKS <= 1) ? 1 : $clog2(NUM_RANKS),
  // A COUNT of ranks up to NUM_RANKS needs clog2(NUM_RANKS+1); an
  // INDEX into NUM_RANKS needs clog2(NUM_RANKS). Both appear here and
  // they are different widths.
  parameter int CNTR_W = $clog2(NUM_RANKS + 1)
) (
  input  logic                   clk,
  input  logic                   rst_n,

  // ── The committed transfer.
  input  logic                   acc_valid,
  input  logic [RK_W-1:0]        acc_rank,
  input  logic                   acc_is_write,
  // The dynamic window for the target, from §14's sequencer.
  input  logic                   dyn_window_open,

  // ── Per-rank assertion decision from Chapter 6.7's odt_policy. This
  //    block does NOT recompute it; duplicating that policy here would
  //    be re-teaching 6.7 and would let the two disagree.
  input  logic [NUM_RANKS-1:0]   odt_bits,

  // ── THE OUTPUT: two bits per rank naming the selected value, packed.
  //    Same encoding as §14: 0=OFF, 1=PARK, 2=NOM, 3=WR.
  output logic [2*NUM_RANKS-1:0] rtt_sel_flat,

  // ── Observability.
  output logic [CNTR_W-1:0]      n_terminating,   // ranks not parked/off
  output logic [CNTR_W-1:0]      n_at_wr,         // ranks presenting WR
  output logic                   rank_invalid,

  output logic                   err_multiple_wr,
  output logic                   err_driver_terminating,
  output logic                   err_dyn_on_nontarget
);

  initial begin
    if (NUM_RANKS < 1)
      $fatal(1, "multi_rank_odt_arbiter: NUM_RANKS must be >= 1");
    if (DYN_ODT_EN && !RTT_NOM_EN)
      $fatal(1, "multi_rank_odt_arbiter: DYN_ODT_EN with RTT_NOM_EN=0");
  end

  localparam logic [1:0] SEL_OFF  = 2'd0;
  localparam logic [1:0] SEL_PARK = 2'd1;
  localparam logic [1:0] SEL_NOM  = 2'd2;
  localparam logic [1:0] SEL_WR   = 2'd3;

  // ── Rank range check. Uses a generate guard rather than a width cast
  //    so a power-of-two rank count does not silently truncate.
  logic rank_bad;
  if (NUM_RANKS >= (1 << RK_W)) begin : g_rk_full
    assign rank_bad = 1'b0;
  end else begin : g_rk_chk
    assign rank_bad = ({1'b0, acc_rank} >= (RK_W+1)'(NUM_RANKS));
  end
  assign rank_invalid = acc_valid && rank_bad;

  logic active;
  assign active = acc_valid && !rank_bad;

  logic [1:0] sel [NUM_RANKS];

  always_comb begin
    for (int r = 0; r < NUM_RANKS; r++) begin
      logic is_target;
      is_target = active && (RK_W'(r) == acc_rank);

      // Default: parked if park is enabled, else nothing selected.
      sel[r] = RTT_PARK_EN ? SEL_PARK : SEL_OFF;

      if (RTT_NOM_EN && odt_bits[r]) sel[r] = SEL_NOM;

      // THE DYNAMIC SWITCH, SCOPED. Only the write target, only while
      // its window is open, and only if the scheme is enabled. The
      // `is_target && acc_is_write` conjunction is §10's whole point.
      if (DYN_ODT_EN && is_target && acc_is_write && dyn_window_open)
        sel[r] = SEL_WR;

      // 6.7's invariant, enforced STRUCTURALLY rather than checked
      // afterwards: a rank that is driving cannot be left at a
      // pin-selected or write-selected value.
      if (is_target && !acc_is_write)
        sel[r] = RTT_PARK_EN ? SEL_PARK : SEL_OFF;
    end
  end

  always_comb begin
    rtt_sel_flat = '0;
    for (int r = 0; r < NUM_RANKS; r++)
      rtt_sel_flat[2*r +: 2] = sel[r];
  end

  // ── Counts. Terminating means presenting something other than OFF
  //    or PARK; park is a real termination electrically but is the
  //    idle baseline, so counting it would hide the interesting number.
  always_comb begin
    n_terminating = '0;
    n_at_wr       = '0;
    for (int r = 0; r < NUM_RANKS; r++) begin
      if ((sel[r] == SEL_NOM) || (sel[r] == SEL_WR))
        n_terminating = n_terminating + CNTR_W'(1);
      if (sel[r] == SEL_WR)
        n_at_wr = n_at_wr + CNTR_W'(1);
    end
  end

  // ── At most one rank can be the write target, so at most one can be
  //    at RTT_WR. More than one means the dynamic scope is broken.
  assign err_multiple_wr = (n_at_wr > CNTR_W'(1));

  // ── A driving rank presenting anything but its parked value.
  logic drv_term;
  always_comb begin
    drv_term = 1'b0;
    if (active && !acc_is_write)
      drv_term = (sel[acc_rank] == SEL_NOM) || (sel[acc_rank] == SEL_WR);
  end
  assign err_driver_terminating = drv_term;

  // ── A non-target rank at RTT_WR: the channel-wide-switch bug.
  logic dyn_leak;
  always_comb begin
    dyn_leak = 1'b0;
    for (int r = 0; r < NUM_RANKS; r++)
      if ((sel[r] == SEL_WR) && !(active && (RK_W'(r) == acc_rank)))
        dyn_leak = 1'b1;
  end
  assign err_dyn_on_nontarget = dyn_leak;

endmodule

The ordering inside the always_comb is deliberate and worth reading as an argument. Park is the baseline, the pin raises it to NOM, the write raises it to WR, and then the read case overwrites everything back down to park — because 6.7's invariant is not a preference to be balanced against the others but a rule that wins outright.

Writing it as a final unconditional override, rather than as a condition inside the earlier branches, is what makes it impossible for a later edit to leave a driving rank terminating.

17. What the Assertions Prove

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// Bind unit note: these properties reference internal state of both
// blocks and are written as though bound into them, so clk and rst_n
// are visible directly. Properties P1-P5 target rtt_state_sequencer;
// P6-P9 target multi_rank_odt_arbiter, whose decision is purely
// combinational and is therefore sampled against the same clock.

// P1 -- THE invariant of Chapter 6.7, now stated over a VALUE. A rank
// that is driving never presents the pin's or the write's termination.
property p_driver_never_terminates;
  @(posedge clk) disable iff (!rst_n)
    err_terminate_while_driving == 1'b0;
endproperty
assert property (p_driver_never_terminates);

// P2 -- the dynamic window returns the selection to RTT_NOM while the
// pin is still asserted, never to RTT_PARK. §4's second easy mistake
// and §15's cycle 8, as a checkable fact.
property p_window_close_returns_to_nom;
  @(posedge clk) disable iff (!rst_n)
    ($fell(dyn_window_open) && odt_assert && RTT_NOM_EN)
      |=> (rtt_sel == 2'd2);
endproperty
assert property (p_window_close_returns_to_nom);

// P3 -- RTT_WR is only ever selected while the dynamic window is open.
property p_wr_only_in_window;
  @(posedge clk) disable iff (!rst_n)
    (rtt_sel == 2'd3) |-> $past(dyn_window_open, 1);
endproperty
assert property (p_wr_only_in_window);

// P4 -- the selection out of reset is never NOM or WR. Both imply a
// transfer, and no transfer has happened.
property p_reset_state_is_safe;
  @(posedge clk)
    $rose(rst_n) |-> (rtt_sel inside {2'd0, 2'd1});
endproperty
assert property (p_reset_state_is_safe);

// P5 -- a selection change is always accompanied by the switching
// flag, so the tADC cost of §5 can never be spent unobserved.
property p_switch_is_reported;
  @(posedge clk) disable iff (!rst_n)
    (rtt_sel != $past(rtt_sel, 1)) |-> $past(switching, 1);
endproperty
assert property (p_switch_is_reported);

// P6 -- at most one rank presents RTT_WR. There is one write target.
property p_at_most_one_wr;
  @(posedge clk) disable iff (!rst_n)
    (n_at_wr <= CNTR_W'(1));
endproperty
assert property (p_at_most_one_wr);

// P7 -- the dynamic switch never reaches a non-target rank. §10's
// channel-wide-switch bug, forbidden.
property p_dyn_scope_respected;
  @(posedge clk) disable iff (!rst_n)
    err_dyn_on_nontarget == 1'b0;
endproperty
assert property (p_dyn_scope_respected);

// P8 -- the arbiter's own invariant check never fires either. Stated
// separately from P1 because the two blocks can fail independently:
// one owns timing, the other owns scope.
property p_arbiter_invariant;
  @(posedge clk) disable iff (!rst_n)
    err_driver_terminating == 1'b0;
endproperty
assert property (p_arbiter_invariant);

// P9 -- with park enabled, no rank is ever left with nothing selected.
// §1's callout: an unterminated idle rank is a stub, and "off" is a
// configuration that must be chosen rather than fallen into.
property p_park_covers_idle;
  @(posedge clk) disable iff (!rst_n)
    RTT_PARK_EN |-> (rtt_sel != 2'd0);
endproperty
assert property (p_park_covers_idle);

// ── Cover: the states and transitions that matter.
cover property (@(posedge clk) disable iff (!rst_n) (rtt_sel == 2'd3));
// The full sequence of §4, as an ordered cover. A regression that
// never hits this has not exercised dynamic ODT at all.
cover property (@(posedge clk) disable iff (!rst_n)
                  (rtt_sel == 2'd1) ##[1:$] (rtt_sel == 2'd2)
                                    ##[1:$] (rtt_sel == 2'd3)
                                    ##[1:$] (rtt_sel == 2'd2)
                                    ##[1:$] (rtt_sel == 2'd1));
// Back-to-back writes: the window closes and reopens without parking.
cover property (@(posedge clk) disable iff (!rst_n)
                  $fell(dyn_window_open) ##[1:4] $rose(dyn_window_open));
// A read immediately after a write to the same rank -- the transition
// from terminating to driving, which is where the invariant is
// easiest to violate.
cover property (@(posedge clk) disable iff (!rst_n)
                  (rtt_sel == 2'd3) ##[1:$] rd_committed_this_rank);
// More than one rank terminating at once: the multi-rank case.
cover property (@(posedge clk) disable iff (!rst_n)
                  (n_terminating > CNTR_W'(1)));
// An overlapping write was attempted and reported.
cover property (@(posedge clk) disable iff (!rst_n) err_overlapping_write);

P2 is the property worth keeping if only one survives review. It is the single behaviour that distinguishes a correct dynamic-ODT model from the plausible wrong one, and it fails immediately for an implementation that treats the window's end as a return to idle.

The second cover — the full PARK → NOM → WR → NOM → PARK sequence — is the one to wire into a regression. It is possible to pass every property above while never actually opening a dynamic window, and a test suite that does so has verified a static termination scheme.

18. Corner Cases

CaseBehaviourWhy
RTT_PARK_EN = 0Idle ranks select OFFLegal and rarely wanted — §1's callout; P9 stops applying
RTT_NOM_EN = 0ODT pin has no effect on the selection§8 — the pin is ignored when RTT_NOM is disabled
DYN_ODT_EN with RTT_NOM_EN = 0$fatal at elaborationThe scheme switches away from RTT_NOM; with the pin inert there is nothing to switch from
WL < 3$fatal at elaborationDODTL underflows and becomes a huge unsigned latency — §5's callout
WL = 3 exactlyDODTL = 0, effect is immediateHandled by an explicit path, not by falling through the counter
NUM_RANKS = 1Non-target branch unreachableDegenerate and worth testing; matches 6.7's same corner
Read committed to a rank mid-windowInvariant forces park; err_terminate_while_driving if timing overlapsA turnaround that overlaps a window is a scheduling error upstream
Second write while a window is openerr_overlapping_writeBack-to-back writes should extend, not restart — §19's DV table
RTT_WR = Hi-Z configuredModelled as a selection like any other§2 — “no termination during a write” is a real configuration
Invalid rank indexAll ranks fall back to park/offSafe direction: under-terminating is suboptimal, terminating a driver is wrong

Row three is the guard most likely to be thought pedantic and it is not. A system that disables RTT_NOM for a debug experiment and leaves dynamic ODT enabled has a configuration whose intent is unrealisable, and without the guard the model would silently present RTT_WR on writes and nothing otherwise — which is neither what was configured nor an error anyone would notice.

19. DV — Model the Device's Termination State

The checker must not re-implement the priority chain, or it will agree with the DUT's bugs. What it should do is model the device as a small independent state machine over the three values, driven only by observable events, and compare.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// Independent device-side termination model. Knows nothing about the
// DUT's implementation; reconstructs the expected selection from the
// pin and the command stream alone.
// SIMULATION-ONLY -- a reference model, not synthesisable intent.
class rtt_device_model;
  int unsigned dodtl;        // WL - 3
  int unsigned wr_cycles;
  bit park_en, nom_en, dyn_en;

  // Event-driven expectation, deliberately computed a different way
  // from the DUT: by timestamping events rather than by counting down.
  int unsigned t_pin_eff;    // when the pin becomes effective
  int unsigned t_win_open;   // when the dynamic window opens
  int unsigned t_win_close;

  function int unsigned expected(int unsigned now, bit pin_eff_now);
    if (dyn_en && now >= t_win_open && now < t_win_close) return 3; // WR
    if (nom_en && pin_eff_now)                            return 2; // NOM
    if (park_en)                                           return 1; // PARK
    return 0;                                                        // OFF
  endfunction
endclass
CheckWhat it establishes
Expected selection equals rtt_sel on every cycle of a single writeThe priority chain and both latencies together
rtt_sel is NOM on the cycle after the window closes, with the pin assertedP2, computed independently rather than asserted
Sweep WL from 3 to 20; the window's open cycle tracks WL − 3The specification's relationship, not a hard-coded lead
Sweep WR_CYCLES; the window's width matches exactlyNo off-by-one at either end of the burst
Drive a read into the middle of a window; expect park and an error flagThe invariant under the worst ordering
Multi-rank: write to rank k, assert ODT on all ranks; expect exactly one WR§10's scope, independently counted
Toggle RTT_NOM_EN off; expect the pin to have no effect at all§8
Count cnt_switches against the model's transition countEvery tADC spend accounted for
Back-to-back writes with no gap; compare window behaviour against the model§20's first debugging row

That last check is where an interesting disagreement shows up:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  DYNAMIC ODT ON BACK-TO-BACK WRITES
    WL = 12 so DODTL = 9; WR_CYCLES = 4; ODT held asserted throughout.

    two writes committed at t=0 and t=4 (back to back, no gap)

    model expectation
      write 0 : window opens t=9,  closes t=13
      write 1 : window opens t=13, closes t=17
      selection: NOM until 9, WR from 9 to 17 CONTINUOUSLY, NOM after

    DUT as written in §14
      write 0 : dyn_pending set at t=0, window opens t=9, dyn_left=4
      write 1 : committed at t=4 -- dyn_pending is still SET, so
                err_overlapping_write fires and the second write's
                lead is RESTARTED from t=4, opening at t=13
      selection: NOM until 9, WR 9..13, then WR again 13..17

    observed: the selection happens to be correct, and the error flag
              fired. Those two facts are both true and they are not
              consistent with each other.

    diagnosis : the DUT models ONE pending window. A second write
              arriving during the lead of the first is a legal traffic
              pattern -- tCCD_L allows back-to-back writes closer
              together than DODTL -- so err_overlapping_write is
              firing on correct traffic.
    what it is NOT : a selection bug. The output is right here by
              coincidence of these particular numbers.
    the real finding : the block needs a window QUEUE, or the error
              must be redefined as "a second window opened before the
              first closed", which is a different and legal condition.
              §21's exercise 3 asks for the fix.

That report is the most valuable thing this testbench produces, and it is worth being precise about why. The selection output is correct and an error flag is asserted on legal traffic. Neither symptom alone would be investigated — the output looks fine, and the error looks like it must indicate something. Together they say the model's notion of “overlapping” is wrong rather than its notion of termination.

20. Debugging

SymptomLikely causeHow to confirm
Marginal at rate, no functional failureWrong termination value or wrong lead6.7 §3's signature; read back MPR page 2 (§8)
Worked at DDR4-2133, marginal at 2400CWL changed, ODT lead did not — §5Recompute DODTLon = WL − 3 for the new bin
Termination never engagesRTT_NOM disabled in MR1 — §8The pin is ignored; check MR1 A10:A8, not the pin
Termination engages but never switchesRTT_WR = 00, dynamic ODT offMR2 A10:A9; the write shows NOM throughout
Every idle rank over-terminatingDynamic switch applied channel-wide — §10err_dyn_on_nontarget; count ranks at WR
Termination drops between back-to-back writesWindow close modelled as a return to park — §4P2; watch the cycle after dyn_open falls
Higher static power than budgetedRTT_PARK set too strong, or asserted too widelycnt_switches and the terminating count over a window
Power fine, margin poor on writesRTT_WR too weak, or Hi-Z configured unintentionallyMR2 A10:A9 — 11 is Hi-Z, not a resistance
Fails only during per-device trainingDynamic ODT unsupported in PDA mode — §4's calloutTermination behaviour differs in that mode by specification
ODT behaves oddly only after power-downMR5's ODT-input-buffer bit — §7That bit is ignored when RTT_NOM is disabled
Init unreliable, ODT-relatedODT not held low through initialisation — §8The pin must be static low until tDLLK and tZQinit expire
Readback disagrees with intended configAn MRS write did not landMPR page 2 is the direct check (§8)

Row two is the one that catches real designs. A speed-grade change updates CWL as a matter of course, and DODTLon moves with it silently — so a port that works functionally at the new rate has moved its termination relative to its data by exactly the CWL delta, and the only symptom is reduced margin.

Row nine is worth remembering because it is asymptomatic outside training. A system whose reference-level training is unreliable while everything else is fine may be hitting a termination difference the specification mandates, not a training bug.

21. Misconceptions

“ODT is on or off.” §1. There are three separately programmed values, and which one is presented depends on the pin and on what the device is doing.

“The ODT pin controls termination.” §8. The pin selects between RTT_PARK and RTT_NOM, and it is ignored entirely when RTT_NOM is disabled in MR1.

RTT_PARK is an optional extra.” §1's callout. It is the value presented for most of a channel's life, because most ranks are idle most of the time.

“Dynamic ODT is triggered by the ODT pin.” §4. It is triggered by the write, which the device learns from the command stream. Termination changes without the pin changing.

“After a write the rank returns to RTT_PARK.” §4 and §15's cycle 8. It returns to RTT_NOM, because the pin is still asserted.

“The dynamic switch applies to the whole channel.” §10. It applies to the write target only. Applying it channel-wide over-terminates every idle rank on every write.

RZQ/2 is a resistance.” §3. It is a ratio against a reference established by ZQ calibration, whose absolute value the controller never learns.

RTT_WR must be a resistance.” §2. 11 selects Hi-Z — explicitly no termination during a write, which some topologies want.

“ODT timing is a fixed number of cycles.” §5. DODTLon = WL − 3, so it moves with CWL and additive latency. Hard-coding it breaks on a speed-grade change.

RTT_NOM and RTT_PARK need different code tables.” §2. They share the same eight codes with the same meanings — a real simplification for firmware.

“Termination stops mattering when the clock stops.” §7. The pins are still on the net, which is why an asynchronous ODT mode and a power-down buffer control both exist.

“If the assertions pass, the termination is right.” §17's callout. Every property here concerns names and cycles; none reaches impedance.

22. Interview Reasoning

How many termination values does a DDR4 device have, and what selects between them? Three — RTT_PARK, RTT_NOM and RTT_WR, in MR5, MR1 and MR2. The ODT pin selects between park and nominal; a write to that rank selects the write value when dynamic ODT is enabled.

Which one is presented most of the time? RTT_PARK, because most ranks are idle most of the time. It is also the one most often left disabled, which leaves idle devices as unterminated stubs on the net.

What actually triggers the dynamic switch? The write, not the pin. The device knows from the command stream that it is being written, and switches for the burst's duration.

After the burst, what does the rank present? RTT_NOM, assuming the pin is still asserted — not park. Getting this wrong drops termination between back-to-back writes, which is when it matters most.

Why is DODTLon expressed as WL − 3 rather than a number? Because termination has to be correct when the write data arrives, and write latency is what locates the data. Tying the two together means the ODT lead automatically tracks CWL and additive latency.

A design moves from DDR4-2133 to 2400 and margin degrades with no functional failure. First hypothesis? CWL changed with the speed bin and the ODT lead did not, so termination now engages at the wrong time relative to the data.

Why are termination values expressed as RZQ/n? So the code selects a ratio against a reference that ZQ calibration has trimmed against an external precision resistor. Process and temperature variation is absorbed by the calibration instead of appearing in the register value — at the cost that the controller never knows the absolute resistance.

RTT_WR has a code for Hi-Z. Why would anyone use it? Because in some topologies the extra loading a strongly terminated write target presents costs more than the reflected energy it absorbs. Turning termination off during the write is then the better trade.

Two ranks, ODT asserted on both, a write to rank 0. What does each present? Rank 0 presents RTT_WR while its window is open; rank 1 presents RTT_NOM. Rank 1 does not follow rank 0 into RTT_WR.

What does a passing set of ODT assertions prove about signal quality? Nothing. Every property is about which value is named and on which cycle; impedance appears nowhere in the digital domain, and the controller cannot even determine the absolute value of the ratio it selected.

23. Exercises

  1. §14's sequencer uses one counter for both DODTLon and DODTLoff because the specification makes them equal. Write the version that carries two independent parameters, then argue whether the extra generality is worth the two extra registers given what §5 says about their values.

  2. Set RTT_PARK_EN = 0 and run §19's model. Which property stops constraining the design, and which debugging row in §20 becomes reachable? Explain why “off” is a configuration rather than an absence.

  3. §19's failure report shows err_overlapping_write firing on legal back-to-back writes. Implement the fix as a two-entry window queue, and state the new error condition precisely. Then explain why redefining the error is a defensible alternative to the queue, and what it costs.

  4. The arbiter enforces 6.7's invariant as a final unconditional override rather than as a condition inside the earlier branches. Rewrite it the other way, then construct the edit to that version that reintroduces a driving rank terminating. What does the comparison say about where invariants belong in a priority chain?

  5. Using the verified tables in §2, write the firmware routine that programs all three values plus driver impedance, in an order that respects §8's initialisation requirement. Identify every ordering in your routine that is mandatory and every one that is merely conventional.

  6. cnt_switches counts selection changes because each spends tADC. Construct a traffic pattern that maximises switches per transfer, and one that minimises them, with the same number of writes. What controller-level policy does the comparison suggest, and which module owns whether that policy is worth pursuing?

  7. Extend §16's arbiter to report, for each cycle, the number of ranks presenting each of the four selections. Then explain why that vector is a better power proxy than n_terminating, and why it is still not a power figure.

  8. §4's callout notes dynamic ODT is unsupported in Per-DRAM Addressability mode, which Module 21 needs for per-device reference training. Work out what termination the device presents during that training, and argue whether a setting trained under those conditions is valid for normal traffic.

24. Where This Goes

Termination is now a decision with a value and a deadline. Three programmable resistances selected by a pin and a command, a dynamic switch scoped to one rank, a latency tied to write latency and therefore to the speed bin, and a park state that covers the intervals nobody thinks about.

And every bit of it is a name. §17's callout is the honest summary: two blocks, nine properties, a full sequence covered, and not one signal anywhere that carries an impedance. The controller selects a ratio against a reference it cannot read, applies it on a cycle it can compute, and has no way to observe whether the result helped.

Which leaves the obvious question unanswered. Termination absorbs something — what?

Chapter 22.2 answers it: where reflections come from, why an impedance discontinuity creates them, why they arrive late and therefore attack a later bit than the one that caused them, and what a terminated channel actually does to that energy. It is also the chapter where the RTL has the least to say and the reasoning has the most — and it is where the narrow, fragmented pass regions Module 21 kept measuring finally acquire a cause.

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.