Skip to content
VLSI Mentor

DDR · Module 27

UVM Architecture for DDR

Three interfaces that are not variations of each other. They share no clock, no transaction identity, and no notion of what a failure is — and the cross-bank obligations finally need a component.

Chapters 27.1 through 27.6 produced parts: a compliance obligation with an honest residual, a property set that publishes its own antecedents, a monitor with a three-valued resolution flag, a distributed scoreboard with five separate totals, a coverage model with a reviewed denominator, and an integration contract for components somebody else wrote.

Nothing has said how they go together, and the answer is not one agent.

The UVM track owns the vocabulary. uvm/agent-architecture owns what an agent is and when to make it active or passive; uvm/multi-agent-systems owns how several coordinate, share a virtual sequencer, and report into one environment.

What neither covers is that a DDR environment has at least three interfaces that are not variations of each other. A host-side request interface where transactions have addresses and ordering. A DDR-side command interface where there are only pins and timing. A sideband interface that is a different protocol entirely. They share no clock, no transaction, and no notion of what a failure is — and 27.4 §6's cross-bank gap, left unowned by every chapter since, finally needs a component of its own.

1. Three Interfaces, Not One

The reason a DDR environment is not one agent is that the three interfaces differ in what a transaction is, not merely in width or speed.

Host sideDDR sideSideband
What arrivesa request — address, size, directionpins — command, address, strobes, dataa register access
Protocolthe SoC's fabric protocolDDR commands and timinga different protocol entirely
Clockthe fabric clockthe DDR clock — 6.1its own, typically far slower
A transaction isgiven to you, wholereconstructed27.3a single read or write
Orderingexplicit, and part of the contractthe controller's choiceModule 23none worth modelling
A failure isa wrong response or a lost requesta timing violation or illegal commanda wrong register value
Data correctnesscheckable — 27.4checkable at the pinsnot applicable

Row four is the structural difference and it is why one agent cannot serve two of these. A host-side monitor is handed a transaction. A DDR-side monitor must derive one, which is the whole of 27.3 — including its three-valued resolution flag, which exists because derivation can fail.

Row six is the difference that breaks a shared scoreboard. A host-side failure and a DDR-side failure are not the same kind of event: a timing violation is a protocol failure with no wrong data, and a wrong response is a data failure with no protocol violation. A component that reports one number for both has merged two questions.

And row three is the difference that breaks a shared time base, which §5 is about.

2. What They Do Not Share

Three shared things a single-agent environment assumes, and none of them survives.

No common clock. CURRICULUM-DERIVED from 6.1 and 19.2: the DDR interface runs at the DDR clock and samples on both edges, while the fabric runs at its own frequency. So there is no cycle number both sides agree on — and §5 shows what that costs a scoreboard.

No common transaction. A host request may become several DDR bursts, or none at all if the controller merges it with another, or several requests may become one burst. CURRICULUM-DERIVED from Module 23: the mapping is the controller's decision and is not one-to-one in either direction.

No common notion of failure. §1's row six. So each domain needs its own checker with its own outcome vocabulary, and merging them produces a number that is meaningless in both.

AssumptionWhere it failsWhat breaks if you assume it anyway
One clock6.1any ordering comparison across domains — §5
One transaction identityModule 23request-to-burst matching; every scoreboard
One failure vocabulary§1 row sixa pass/fail number that means neither thing
One address space view8.6host addresses are flat; DDR addresses are bank/row/column

The fourth row is the one an environment silently gets wrong. A host-side address and a DDR-side bank/row/column are the same location expressed in two forms, and the translation is the controller's address mapping. So a cross-domain scoreboard needs a model of that mapping — which is a fifth reference model, and one whose errors correlate with the design's own by exactly 27.6 §9's mechanism.

3. The Three Agents

Each interface gets an agent, and the differences in §1 determine what each agent can contain.

The host-side agent is conventional. A driver issuing requests, a sequencer, a monitor that receives whole transactions. Its monitor needs no resolution flag — nothing is being derived. This is the agent the UVM track's agent-architecture describes without modification.

The DDR-side agent is the unusual one, and 27.3 is its monitor. It is passive in a controller testbench — the DUT drives the pins and the agent observes — and active in a device or PHY testbench, where it must drive commands. CURRICULUM-DERIVED from 27.3 §4: its monitor sees pins only and deliberately holds no DUT handle.

The sideband agent is small and is usually omitted, which is a mistake with a specific consequence. Module 21 owns training, which is driven through register accesses; Module 24 owns power-state transitions, which are commanded the same way. So an environment with no sideband agent cannot test training or a power-state change — it can only start from an already-initialised device.

AgentModeIts monitorContains
Hostactiveconventionaldriver, sequencer, monitor
DDRpassive (controller DUT) or active (device DUT)27.3'smonitor always; driver only when active
Sidebandactiveconventionalregister sequences — Module 21

The mode column is where the VIP question from 27.6 §6 lands. A memory-model VIP occupies the DDR-side agent's active position; a passive VIP sits beside 27.3's monitor in the same agent. The agent boundary is therefore where a VIP substitutes, which is the architectural reason 27.6 §14's overlap plan is a per-component decision rather than a per-environment one.

4. The Device-Level Component

Chapter 27.4 §6 named a gap and could not close it: every cross-bank obligation loses its owner when a scoreboard is distributed by bank. Six chapters later, this is where it gets a component.

CURRICULUM-DERIVED, the obligations and why no per-bank component sees them:

ObligationOwnerWhy it is device-level
Rolling activate window14.8the deviceit counts activates in any bank
RefreshModule 15the devicea refresh command addresses all banks
Bank-group spacing5.3pairs of banksthe relation is between two bank models
Power state6.2the deviceper-bank state says nothing about it
Cross-bank orderingthe controllereach bank sees only its own accesses

Chapter 27.4 §6 argued that centralising is the wrong fix, because a central scoreboard enlarges every ambiguity set — ambiguity is bounded by accesses in flight to one address, and addresses in different banks cannot collide. So the resolution is a separate component, not a merged one, and that is an environment-structure decision, which is why it waited for this chapter.

The component is device-level, not bank-level, and it checks obligations rather than data. It consumes the same transaction stream the sixteen scoreboards consume, counts across all of them, and has no reference memory at all. §10's property set is the bindable form of the same obligations at the pins.

5. No Common Time Base

§2's first row said there is no cycle number both domains agree on. Here is what that costs.

A scoreboard needs to know which of two events happened first. Chapter 27.4 §3 established that the expected value of a read depends on which writes preceded it, so ordering is the scoreboard's central input.

Within one domain, ordering is free — the clock provides it. Across domains it does not exist, and the two available substitutes are both wrong in a stated way:

SubstituteHow it worksWhat it gets wrong
Simulation timecompare $realtimecorrect, and finer than either protocol's resolution — two events in one DDR cycle get an order the protocol does not define
Domain cycle countscompare cycle numbersincomparable — different clocks, no fixed ratio if either is dynamic
Causality onlyorder only events with an observed dependencycorrect and partial — most pairs get no order at all

The third row is the honest one and it is weaker than it looks. Ordering only causally related events means a host request and a DDR burst are ordered when the burst was observed after the request was accepted — and two host requests whose bursts interleave get no order, which is precisely 27.4 §3's ambiguity arriving from a second direction.

And simulation time is the trap, because it always works. $realtime gives every pair of events an order, so a cross-domain scoreboard built on it never reports ambiguity. It has invented an ordering the protocol does not define — two commands in the same DDR cycle are simultaneous to the protocol and ordered by the simulator, and a checker relying on that distinction passes in simulation and describes nothing real.

CURRICULUM-DERIVED consequence: a cross-domain scoreboard should produce 27.4's set far more often than a single-domain one, and §11's correlator reports how often it could not order a pair — because a correlator that always succeeds is using simulation time.

6. Bringing the Environment Up

Three agents that share no clock cannot start simultaneously, and the order is forced by two constraints from earlier chapters pulling in opposite directions.

Chapter 27.3 §6 established that a monitor attaching mid-traffic cannot resolve bank state. It sees a read to a bank whose open row it never observed being activated, so it reports unknown — correctly, and uselessly. So the DDR-side monitor must be observing before the first command.

Module 21 establishes that a device is not usable until training completes, and training is driven from the sideband. So the sideband agent must run before host traffic means anything.

PhaseActiveWhy this order
1DDR-side monitor only — passive27.3 §6 — it must see the first command
2Sideband — bring-up6.8 — reset release, before the device is addressable
3Sideband — trainingModule 21 — and the monitor must not score it
4Host — trafficthe only phase where 27.4's scoreboards mean anything
5Sideband, concurrentlymode-register and power changes — §12

Phase three is the one that breaks a naive environment, and the reason is specific. Training drives commands and data patterns on the DDR interface that are not transactions — Module 21 owns what they are — and 27.3's monitor will happily reconstruct them into transactions with meaningless addresses. Those then reach sixteen scoreboards, which compare them against a reference memory that has never been written, and every one becomes an unchecked read.

The consequence is not a failure. It is 27.4 §2's unchecked total inflated by a phase that was never meant to be checked — so a run whose scoreboards report mostly-unchecked reads may simply have scored its own training sequence. CURRICULUM-DERIVED from 27.4 §12: this is exactly why that chapter counted unchecked reads separately rather than folding them into passes, and it is the second place that decision has paid.

So the monitor needs a phase input, and the honest treatment is to discard rather than resolve. A transaction reconstructed during training is not unknown in 27.3 §6's sense — it is not a transaction at all, which is a fourth state that chapter did not need and this one does.

7. Where Each Instrument Goes

Six chapters of components, placed.

ComponentChapterWhere it goesWhy there
Compliance registry27.1 §9environment levelthe obligation is device-wide
Bindable rule checker27.1 §11bound at the DDR interfaceit checks pins
Spacing property set27.2 §11bound at the DDR interfacesame
Vacuity harness27.2 §13environment levelit reports across all properties
DDR monitor27.3 §11DDR-side agentit derives transactions from pins
Reference memory27.4 §10one per bankthe address space partitions — 5.2
Order resolver27.4 §11one per bankambiguity is bounded within a bank
Bank scoreboard27.4 §12one per bank, sixteen of them27.4 §5
Device-level checker§10 hereenvironment level27.4 §6's gap
Coverage model27.5 §12environment level, three groups27.5 §12 — three sampling events
Closure argument27.5 §11environment levelit consumes every model's counts
Capability registry27.6 §10environment levelit describes the whole integration
Boundary property set27.6 §11bound at the VIP boundaryit checks the integration's assumptions
Cross-check27.6 §12DDR-side agentit compares two monitors of one interface
Cross-domain correlator§11 hereenvironment level§5 — it spans domains by definition

Three of the fifteen are bindable property sets at an interface, and the rest are class-based components in the hierarchy. That split is not incidental: a pin-level rule belongs at the pins, where it has a clock and needs no reconstruction, and everything that needs a transaction belongs above the monitor that produces one.

Note that nothing is placed on the host side except the host agent's own monitor. The host-side checking this module built is, deliberately, none: 27.4's scoreboard is banked and therefore DDR-side. A complete environment needs host-side response checking too, and this module did not build itModule 23 owns the controller's obligations to its requester, and that is where it belongs.

8. The Architecture

A DDR verification environment's structure, in four rows. The top row holds the three agents that correspond to the three interfaces — the host-side agent where transactions arrive whole, the DDR-side agent whose monitor must derive transactions from pins, and the sideband agent that carries register accesses for initialisation and power-state changes — together with the design under test. The second row holds the per-bank components: sixteen reference memories, sixteen order resolvers, and sixteen bank scoreboards, all fed from the DDR-side agent's monitor. The third row holds the three environment-level components that no per-bank view can provide: the device-level checker that owns the cross-bank obligations left unowned since chapter 27.4, the cross-domain correlator that must order events across clocks that share no time base, and the coverage model. The bottom row is the closure argument, which consumes all of them and refuses to reduce them to a single number. Edges run from the design into all three agents, from the DDR-side agent into the per-bank components and the device-level checker, from the host and sideband agents into the correlator, from the per-bank components into the correlator and the coverage model, and from the device-level checker, correlator and coverage model into the closure argument.DUTcontroller, device or PHYHost agentrequests arrive whole —§1DDR-side agent27.3's monitor — pinsonlySideband agentinit, training, power —§3Ref memory x1627.4 §10 — one per bankOrder resolver x1627.4 §11 — a value or asetBank scoreboard x1627.4 §12 — five totalseachBound property sets27.1, 27.2 — at the pins— §7Device-level checker27.4 §6's gap, owned — §4Cross-domaincorrelatorno common time base — §5Coverage model27.5 — three groupsClosure argument27.5 §11 — four numbers12

Three things the structure makes visible.

The device-level checker hangs off the DDR-side agent and nothing else. §4 establishes that its obligations have no per-bank projection, so it takes the raw transaction stream rather than the scoreboards' outputs — a component built from sixteen summaries could not count a rolling window.

The correlator is the only component fed from all three agents. That is §5's problem drawn: it is the one place where events from clocks with no fixed ratio must be ordered, and it is therefore the one place where simulation time is a tempting and wrong answer.

And everything terminates at the closure argument rather than at a pass/fail. Chapter 27.5 §11's component refuses to produce a single percentage; this diagram is the reason that refusal matters architecturally — five sources of evidence converge, and no one number can carry what any of them found.

9. The Topology Component

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ---------------------------------------------------------------------
// ddr_env_topology -- records the environment's structure and refuses
// the topologies the preceding chapters prove are blind.
//
// CLASSIFICATION: educational, SIMULATION-ONLY.
//
// WHY REFUSALS AND NOT WARNINGS: each of the three below removes a
// class of finding entirely rather than degrading one.
//   - no device-level checker: 27.4 §6's cross-bank obligations have
//     no owner, and §4's worked failure passes sixteen scoreboards.
//   - no sideband agent: Module 21's training and Module 24's power
//     transitions are commanded through register access, so the
//     environment can only start already-trained.
//   - a correlator with no ordering policy: §5 -- it will fall back
//     to simulation time, which orders events the protocol defines
//     as simultaneous and therefore never reports ambiguity.
//
// WHAT IT DOES NOT MODEL:
//   - the UVM factory, phasing, or build_phase (the UVM track owns
//     those); this records the PLAN, not the built hierarchy
//
// WHAT IT WOULD MISS:
//   - a topology declared here and built differently. A declared
//     device-level checker nobody connected passes every check.
// ---------------------------------------------------------------------
typedef enum { IF_HOST,      // §1: requests arrive whole
               IF_DDR,       // §1: pins, and a transaction is derived
               IF_SIDEBAND   // §1: a different protocol entirely
             } ddr_if_e;

typedef enum { AG_ABSENT, AG_PASSIVE, AG_ACTIVE } ag_mode_e;

// §5's three substitutes for a time base. ORD_SIM_TIME is included
// so it can be REFUSED by name rather than merely omitted.
typedef enum { ORD_NONE,       // no policy declared -- refused
               ORD_SIM_TIME,   // always works, invents an order
               ORD_CAUSAL      // correct and partial -- §5's row three
             } order_policy_e;

class ddr_env_topology extends uvm_object;
  `uvm_object_utils(ddr_env_topology)

  protected ag_mode_e      m_agent[ddr_if_e];
  protected bit            m_device_level_checker;
  protected bit            m_coverage_model;
  protected bit            m_closure_argument;
  protected int            m_bank_scoreboards;
  protected order_policy_e m_order_policy;
  protected string         m_refusal[$];

  function new(string name = "ddr_env_topology");
    super.new(name);
    m_agent[IF_HOST]     = AG_ABSENT;
    m_agent[IF_DDR]      = AG_ABSENT;
    m_agent[IF_SIDEBAND] = AG_ABSENT;
    m_order_policy       = ORD_NONE;
  endfunction

  function void declare_agent(ddr_if_e which, ag_mode_e mode);
    m_agent[which] = mode;
  endfunction

  // §3: a DDR-side agent is passive against a controller DUT and
  // active against a device or PHY. Both are legal; ABSENT is not,
  // because 27.3's monitor is the only source of transactions.
  function ag_mode_e agent_mode(ddr_if_e which);
    return m_agent[which];
  endfunction

  function void declare_bank_scoreboards(int n);
    if (n < 1) begin
      `uvm_error("TOPO", "a distributed scoreboard needs at least one bank")
      return;
    end
    m_bank_scoreboards = n;
  endfunction

  function void declare_device_level_checker(bit present);
    m_device_level_checker = present;
  endfunction

  function void declare_coverage_model(bit present);
    m_coverage_model = present;
  endfunction

  function void declare_closure_argument(bit present);
    m_closure_argument = present;
  endfunction

  // §5: the policy must be stated. ORD_SIM_TIME is refused by name
  // rather than silently accepted, because it is the substitute that
  // always works and therefore never reports ambiguity.
  function void declare_order_policy(order_policy_e p);
    m_order_policy = p;
  endfunction

  protected function void refuse(string why);
    foreach (m_refusal[i]) if (m_refusal[i] == why) return;
    m_refusal.push_back(why);
  endfunction

  // The three structural refusals, plus the two that follow from
  // 27.4 and 27.5. Recomputed on each call so a later declaration
  // clears an earlier refusal.
  function bit sound(ref string problems[$]);
    m_refusal.delete();

    if (m_agent[IF_DDR] == AG_ABSENT)
      refuse("no DDR-side agent: 27.3's monitor is the only source of transactions");

    if (m_bank_scoreboards >= 1 && !m_device_level_checker)
      refuse("distributed scoreboards with no device-level checker: 27.4 §6's cross-bank obligations are unowned");

    if (m_agent[IF_SIDEBAND] == AG_ABSENT)
      refuse("no sideband agent: training and power transitions cannot be commanded (Modules 21, 24)");

    if (m_order_policy == ORD_NONE)
      refuse("no ordering policy declared: §5 -- the fallback is simulation time");

    if (m_order_policy == ORD_SIM_TIME)
      refuse("simulation time as an ordering policy: §5 -- it orders events the protocol defines as simultaneous");

    if (m_coverage_model && !m_closure_argument)
      refuse("a coverage model with no closure argument: 27.5 §11 -- a percentage with nothing to qualify it");

    problems = m_refusal;
    return m_refusal.size() == 0;
  endfunction

  // §4: the device-level checker consumes the RAW stream, not the
  // scoreboards' summaries. A component built from sixteen summaries
  // could not count a rolling window, because summing per-bank
  // totals over a run discards the times.
  function bit device_checker_sees_raw_stream();
    return m_device_level_checker && (m_agent[IF_DDR] != AG_ABSENT);
  endfunction

  function int refusals();  return m_refusal.size();  endfunction

  function void report_phase(uvm_phase phase);
    string problems[$];
    bit ok = sound(problems);
    `uvm_info("TOPO", $sformatf(
      "host=%s ddr=%s sideband=%s banks=%0d devchk=%0b cov=%0b closure=%0b order=%s sound=%0b",
      m_agent[IF_HOST].name(), m_agent[IF_DDR].name(),
      m_agent[IF_SIDEBAND].name(), m_bank_scoreboards,
      m_device_level_checker, m_coverage_model, m_closure_argument,
      m_order_policy.name(), ok), UVM_LOW)
    foreach (problems[i])
      `uvm_error("TOPO", $sformatf("topology refused: %s", problems[i]))
  endfunction
endclass

ORD_SIM_TIME exists in the enumeration so it can be refused by name. §5 establishes that simulation time is the substitute that always works — so an environment with no declared policy silently gets it. Naming it as a value makes the refusal specific, and ORD_NONE is refused too for exactly the reason that the fallback is invisible.

And sound() recomputes rather than accumulating. A refusal cleared by a later declaration must disappear, or the component would report a problem the environment has fixed — the same reason 27.4 §10's model lets a write clear an invalidation.

10. The Device-Level Property Set

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ---------------------------------------------------------------------
// ddr_device_level_check -- CLASSIFICATION: synthesisable, BINDABLE.
//
// 27.4 §6's cross-bank obligations, at the pins. Every rule here
// counts across banks, which is exactly why no per-bank component can
// see it -- §4's callout works the rolling-window case through.
//
// Every property is guarded by `disable iff (!rst_n)`: 27.2's census
// measured 769 of this curriculum's 843 named properties with such a
// guard and 74 without, and the 74 are where reset-time noise comes
// from.
// ---------------------------------------------------------------------
module ddr_device_level_check #(
  parameter int BANKS       = 16,
  parameter int GROUPS      = 4,
  parameter int FAW_WINDOW  = 30,   // ILLUSTRATIVE cycles; Module 14 owns real values
  parameter int FAW_LIMIT   = 4,    // 14.8: activates permitted in the window
  parameter int TREFI       = 3900  // ILLUSTRATIVE cycles between refreshes
)(
  input  logic                      clk,
  input  logic                      rst_n,
  input  logic                      act_valid,
  input  logic [$clog2(BANKS)-1:0]  act_bank,
  input  logic                      pre_valid,
  input  logic                      pre_all,        // precharge-all
  input  logic                      ref_valid,
  input  logic                      pd_entry,       // power-down entry
  input  logic                      pd_exit,
  input  logic                      any_cmd_valid
);
  // COUNT vs INDEX: a counter holding up to N needs $clog2(N+1) bits,
  // not $clog2(N), and the ternary guards the degenerate case -- a bare
  // $clog2 is 0 at 1, giving an illegal [-1:0] vector.
  //
  // AND THE WIDTH MUST BE CHOSEN FOR WHAT THE CHECKER MUST REPRESENT,
  // WHICH IS NOT FAW_LIMIT. In correct operation the window holds at
  // most FAW_LIMIT activates -- but this checker exists to detect the
  // case where it holds MORE, and a counter sized for FAW_LIMIT
  // saturates exactly in the failure it is meant to report. Sizing it
  // for the legal maximum would make the count wrong precisely when it
  // matters, which is the same shape of error as a checker that cannot
  // fail. So it is sized for FAW_WINDOW.
  localparam int CNT_W   = (FAW_WINDOW < 2) ? 1 : $clog2(FAW_WINDOW + 1);
  // Kept for the contrast: this is the width a counter for the LEGAL
  // maximum would have, and using it for act_in_window is the bug.
  localparam int LIMIT_W = (FAW_LIMIT + 1 < 2)
                         ? 1 : $clog2(FAW_LIMIT + 1);

  initial begin
    if (BANKS < 2)
      $fatal(1, "ddr_device_level_check: BANKS must be >= 2 (got %0d)", BANKS);
    if (GROUPS < 1 || (BANKS % GROUPS) != 0)
      $fatal(1, "ddr_device_level_check: %0d banks do not divide into %0d groups",
             BANKS, GROUPS);
    if (FAW_LIMIT < 1)
      $fatal(1, "ddr_device_level_check: FAW_LIMIT must be >= 1");
    if (FAW_WINDOW < 2)
      $fatal(1, "ddr_device_level_check: FAW_WINDOW must be >= 2");
  end

  // ---- A shift register over the window is the only structure that
  // can answer "how many activates in the LAST N cycles". A running
  // total cannot: it would need to know what to subtract, which is
  // the time information a per-bank summary discards -- §4.
  logic [FAW_WINDOW-1:0]   act_hist;
  logic [CNT_W-1:0]        act_in_window;

  always_ff @(posedge clk) begin
    if (!rst_n) begin
      act_hist      <= '0;
      act_in_window <= '0;
    end else begin
      // Compute from the PRE-shift history plus the incoming
      // activate, so the count and the history describe the same
      // window. Reading act_hist after shifting it would be the
      // stale-register hazard this curriculum has found five times.
      act_in_window <= ($countones(act_hist[FAW_WINDOW-2:0]))
                     + (act_valid ? 1 : 0);
      act_hist      <= {act_hist[FAW_WINDOW-2:0], act_valid};
    end
  end

  // ---- 14.8: the rolling window. This is the obligation §4's
  // callout proves has no per-bank projection.
  property p_faw_respected;
    @(posedge clk) disable iff (!rst_n)
      act_valid |-> ($countones(act_hist[FAW_WINDOW-2:0]) < FAW_LIMIT);
  endproperty
  assert property (p_faw_respected)
    else $error("device: activate %0d inside a window of %0d already holding %0d",
                act_bank, FAW_WINDOW, $countones(act_hist[FAW_WINDOW-2:0]));

  // ---- INVARIANT, not an implication: the window count never
  // exceeds the limit at any cycle. 27.2's census found 21.47% of
  // properties structurally immune to vacuity, and this is one.
  property p_window_count_bounded;
    @(posedge clk) disable iff (!rst_n)
      ($countones(act_hist) <= FAW_LIMIT);
  endproperty
  assert property (p_window_count_bounded)
    else $error("device: window holds %0d activates, limit %0d",
                $countones(act_hist), FAW_LIMIT);

  // ---- The bank index must lie inside the configured geometry. A
  // controller that aliases is a device-level error, not a bank one.
  property p_act_bank_in_range;
    @(posedge clk) disable iff (!rst_n)
      act_valid |-> (act_bank < BANKS);
  endproperty
  assert property (p_act_bank_in_range)
    else $error("device: activate to bank %0d outside %0d banks", act_bank, BANKS);

  // ---- Refresh interval tracking. Module 15 owns the obligation;
  // this counts the interval across all banks, which no per-bank
  // component can do because a refresh addresses all of them.
  logic [CNT_W+16:0] since_ref;
  always_ff @(posedge clk) begin
    if (!rst_n)            since_ref <= '0;
    else if (ref_valid)    since_ref <= '0;
    else if (since_ref != '1) since_ref <= since_ref + 1'b1;
  end

  property p_refresh_interval;
    @(posedge clk) disable iff (!rst_n)
      (since_ref < (TREFI * 9));      // ILLUSTRATIVE postponement allowance
  endproperty
  assert property (p_refresh_interval)
    else $error("device: %0d cycles since the last refresh", since_ref);

  // ---- A refresh and an activate in the same cycle is a device-wide
  // conflict: the refresh addresses every bank.
  property p_no_ref_with_act;
    @(posedge clk) disable iff (!rst_n)
      ref_valid |-> !act_valid;
  endproperty
  assert property (p_no_ref_with_act)
    else $error("device: refresh concurrent with an activate");

  // ---- 6.2's command-qualification contract: no command is accepted
  // while the device is powered down. This is UNIVERSAL DDR, not an
  // LPDDR feature -- 24.2 owns the LPDDR state SET, 6.2 owns the
  // contract. Per-bank state says nothing about the device's power
  // state -- §4's fourth row.
  logic powered_down;
  always_ff @(posedge clk) begin
    if (!rst_n)          powered_down <= 1'b0;
    else if (pd_exit)    powered_down <= 1'b0;
    else if (pd_entry)   powered_down <= 1'b1;
  end

  property p_no_cmd_while_down;
    @(posedge clk) disable iff (!rst_n)
      powered_down |-> !any_cmd_valid;
  endproperty
  assert property (p_no_cmd_while_down)
    else $error("device: a command arrived while powered down");

  // ---- INVARIANT: entry and exit never coincide.
  property p_pd_edges_exclusive;
    @(posedge clk) disable iff (!rst_n)
      !(pd_entry && pd_exit);
  endproperty
  assert property (p_pd_edges_exclusive)
    else $error("device: power-down entry and exit in one cycle");

  // ---- Power-down entry requires no activates in the window still
  // settling, which is again a cross-bank condition.
  property p_pd_entry_quiescent;
    @(posedge clk) disable iff (!rst_n)
      pd_entry |-> !act_valid;
  endproperty
  assert property (p_pd_entry_quiescent)
    else $error("device: power-down entry concurrent with an activate");

  // ---- A precharge-all is device-wide, so it may not coincide with
  // a bank-specific activate.
  property p_pre_all_exclusive;
    @(posedge clk) disable iff (!rst_n)
      (pre_valid && pre_all) |-> !act_valid;
  endproperty
  assert property (p_pre_all_exclusive)
    else $error("device: precharge-all concurrent with an activate");

  // ---- A refresh requires the array precharged, which per-bank
  // models can each satisfy while the device does not.
  property p_ref_after_precharge;
    @(posedge clk) disable iff (!rst_n)
      ref_valid |-> $past(pre_valid, 1) || $past(pre_valid, 2)
                 || $past(pre_valid, 3) || (since_ref == '0);
  endproperty
  assert property (p_ref_after_precharge)
    else $error("device: refresh with no preceding precharge");

  // ---- IMPLICATION: pre_all is only meaningful alongside pre_valid,
  // so this is gated and is vacuity-exposed like the rest. A
  // precharge-all implies a precharge.
  property p_pre_all_implies_pre;
    @(posedge clk) disable iff (!rst_n)
      pre_all |-> pre_valid;
  endproperty
  assert property (p_pre_all_implies_pre)
    else $error("device: precharge-all asserted without a precharge");

  // ---- INVARIANT: the history register and the derived count agree.
  // This is the stale-register guard made into a property, because
  // the two are updated in one always_ff and a future edit could
  // reorder them.
  // EQUALITY, not a bound. Both registers are written from the same
  // pre-shift value in one always_ff, so after any edge
  //   countones({hist[W-2:0], act_valid}) == countones(hist[W-2:0]) + act_valid
  // which is exactly what act_in_window received. They are therefore
  // equal every cycle, reset included -- so a loose "<= +1" bound would
  // accept the reordering this property exists to catch.
  property p_count_matches_history;
    @(posedge clk) disable iff (!rst_n)
      (act_in_window == $countones(act_hist));
  endproperty
  assert property (p_count_matches_history)
    else $error("device: window count %0d disagrees with history %0d",
                act_in_window, $countones(act_hist));

  // ---- 27.2 §7: publish every antecedent, or a silent pass is
  // indistinguishable from a set that never armed.
  cover property (@(posedge clk) disable iff (!rst_n) act_valid);
  cover property (@(posedge clk) disable iff (!rst_n) ref_valid);
  cover property (@(posedge clk) disable iff (!rst_n) pre_valid);
  cover property (@(posedge clk) disable iff (!rst_n) pre_valid && pre_all);
  cover property (@(posedge clk) disable iff (!rst_n) pd_entry);
  cover property (@(posedge clk) disable iff (!rst_n) pd_exit);
  cover property (@(posedge clk) disable iff (!rst_n) powered_down);

  // ---- 14.8's interesting bin: an activate with the window already
  // at limit minus one. 27.5 §5 argues AT_MIN is the only value that
  // tests a rule, and this is that value for the rolling window.
  cover property (@(posedge clk) disable iff (!rst_n)
                  act_valid && ($countones(act_hist[FAW_WINDOW-2:0])
                                == FAW_LIMIT - 1));

  // ---- An activate spread across banks inside one window, which is
  // §4's worked failure shape when it is legal.
  cover property (@(posedge clk) disable iff (!rst_n)
                  act_valid && ($countones(act_hist) >= 2));

  // ---- A refresh arriving near the interval limit.
  cover property (@(posedge clk) disable iff (!rst_n)
                  ref_valid && (since_ref > TREFI));

  // ---- And a command attempted at power-down, so the error path's
  // silence is distinguishable from an unexercised one.
  cover property (@(posedge clk) disable iff (!rst_n)
                  pd_entry && $past(any_cmd_valid, 1));
endmodule

A shift register, not a running total, and that is the whole of §4 in one design decision. Answering “how many activates in the last N cycles” requires knowing which activates are leaving the window — and a running total would need to know what to subtract. That is precisely the time information a per-bank summary discards, which is why sixteen per-bank counts cannot be summed into this answer.

The count is computed from the pre-shift history. act_in_window reads act_hist[FAW_WINDOW-2:0] in the same clocked block that shifts it, so both describe one window. Reading the shifted value would be the stale-register hazard this curriculum has now found five times — and p_count_matches_history exists so a future edit that reorders them fails rather than drifts.

And the counter is sized for FAW_WINDOW, not for FAW_LIMIT. This is the correction worth dwelling on, because sizing it for the limit is the obvious choice and is wrong. In correct operation the window holds at most FAW_LIMIT activates — so a counter of $clog2(FAW_LIMIT + 1) bits is wide enough for every legal run. And this checker exists to detect the case where the window holds more, at which point that counter saturates and reports a wrong value precisely in the failure it was built to report.

That is the same shape of error as a checker that cannot fail, arriving through a width rather than a condition: the instrument is correct on every run where it is not needed. LIMIT_W is kept in the source purely to name the wrong choice. The + 1 is still the COUNT-versus-INDEX rule — a counter holding up to N needs N + 1 representable values — and the ternary guards the degenerate case, since a bare $clog2 is zero at one and would declare an illegal [-1:0] vector.

11. The Cross-Domain Correlator

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ---------------------------------------------------------------------
// ddr_cross_domain_correlator -- orders events across domains that
// share no time base, and reports what it could not order.
//
// CLASSIFICATION: educational, SIMULATION-ONLY.
//
// WHY CAUSALITY AND NOT SIMULATION TIME (§5): $realtime gives every
// pair of events an order, so a correlator built on it NEVER reports
// ambiguity -- and two commands in one DDR cycle are simultaneous to
// the protocol and ordered by the simulator. A checker relying on
// that distinction passes in simulation and describes nothing real.
//
// SO THE HONEST RESULT IS PARTIAL: most pairs get no order, which is
// 27.4 §3's ambiguity arriving from a second direction. The number
// this class exists to publish is how often that happened.
//
// WHAT IT DOES NOT MODEL:
//   - either clock, any CDC structure, the controller's address map
//
// WHAT IT WOULD MISS:
//   - every ordering that is real but unobserved. Two host requests
//     whose bursts interleave have a true order inside the
//     controller and no observable one.
// ---------------------------------------------------------------------
typedef enum { DOM_HOST, DOM_DDR, DOM_SIDEBAND } domain_e;

typedef enum { ORD_BEFORE,      // a dependency establishes a precedes b
               ORD_AFTER,
               ORD_UNORDERED    // §5 -- and this is not a failure
             } rel_e;

class dom_event extends uvm_object;
  `uvm_object_utils(dom_event)
  domain_e      domain;
  int unsigned  seq;         // monotonic WITHIN its own domain only
  string        tag;
  int unsigned  depends_on;  // seq of an event it causally follows, or 0
  bit           has_dep;
  function new(string name = "dom_event");
    super.new(name);
    has_dep = 0;
  endfunction
endclass


class ddr_cross_domain_correlator extends uvm_component;
  `uvm_component_utils(ddr_cross_domain_correlator)

  protected dom_event m_ev[$];
  protected int m_ordered;
  protected int m_unordered;
  protected int m_same_domain;
  protected int m_dep_missing;

  function new(string name, uvm_component parent);
    super.new(name, parent);
  endfunction

  function void note(domain_e d, int unsigned seq, string tag,
                     bit has_dep = 0, int unsigned depends_on = 0);
    dom_event e = dom_event::type_id::create("e");
    if (tag == "") begin
      `uvm_error("CORR", "an event needs a tag")
      return;
    end
    e.domain = d; e.seq = seq; e.tag = tag;
    e.has_dep = has_dep; e.depends_on = depends_on;
    m_ev.push_back(e);
  endfunction

  protected function bit find(domain_e d, int unsigned seq,
                              output int idx);
    foreach (m_ev[i])
      if (m_ev[i].domain == d && m_ev[i].seq == seq) begin
        idx = i;
        return 1;
      end
    return 0;
  endfunction

  // The heart of §5. Two events in ONE domain are ordered by that
  // domain's clock. Across domains, only an observed dependency
  // orders them -- and there is deliberately no fallback.
  function rel_e relate(domain_e da, int unsigned sa,
                        domain_e db, int unsigned sb);
    int ia, ib;
    if (!find(da, sa, ia) || !find(db, sb, ib)) begin
      `uvm_error("CORR", "relate() on an event that was never noted")
      return ORD_UNORDERED;
    end

    // Within one domain the clock provides the order, for free.
    if (da == db) begin
      m_same_domain++;
      return (sa < sb) ? ORD_BEFORE : (sa > sb) ? ORD_AFTER : ORD_UNORDERED;
    end

    // Across domains: a dependency, or nothing. NOT $realtime.
    if (m_ev[ib].has_dep && m_ev[ib].depends_on == sa) begin
      m_ordered++;
      return ORD_BEFORE;
    end
    if (m_ev[ia].has_dep && m_ev[ia].depends_on == sb) begin
      m_ordered++;
      return ORD_AFTER;
    end

    m_unordered++;
    return ORD_UNORDERED;
  endfunction

  // A declared dependency whose target was never observed is a
  // finding about the monitors, not about ordering.
  function int dangling_dependencies();
    int n = 0, idx;
    foreach (m_ev[i])
      if (m_ev[i].has_dep && !find(m_ev[i].domain, m_ev[i].depends_on, idx))
        n++;
    m_dep_missing = n;
    return n;
  endfunction

  function int ordered();      return m_ordered;      endfunction
  function int unordered();    return m_unordered;    endfunction
  function int same_domain();  return m_same_domain;  endfunction
  function int events();       return m_ev.size();    endfunction

  // §5: a correlator that orders every cross-domain pair is using a
  // time base the protocol does not define. So an unordered count of
  // ZERO over a non-trivial run is the symptom, not the goal.
  function bit ordering_is_honest();
    if (m_ordered + m_unordered == 0) return 0;   // nothing related
    return m_unordered > 0;
  endfunction

  function void report_phase(uvm_phase phase);
    void'(dangling_dependencies());
    `uvm_info("CORR", $sformatf(
      "events=%0d same_domain=%0d cross_ordered=%0d cross_unordered=%0d dangling_deps=%0d honest=%0b",
      m_ev.size(), m_same_domain, m_ordered, m_unordered,
      m_dep_missing, ordering_is_honest()), UVM_LOW)
    if (m_dep_missing != 0)
      `uvm_error("CORR", $sformatf(
        "%0d declared dependencies name an event nobody observed", m_dep_missing))
  endfunction
endclass

ordering_is_honest() returns false when nothing was left unordered, and that inversion is the section's whole argument. A correlator that successfully orders every cross-domain pair has a time base the protocol does not define — so a zero unordered count over a real run is a symptom rather than an achievement.

And there is deliberately no fallback in relate(). §5's table puts simulation time first because it always works; omitting it is not an oversight but the design, and 27.6 §13's topology refusal of ORD_SIM_TIME is the same decision enforced one level up.

12. Coordinating Stimulus Across Three Domains

§5 was about ordering events you observe. This is the same problem for events you cause, and it is harder, because a virtual sequence must decide when to issue something on a domain whose clock it cannot count.

The UVM track's multi-agent-systems owns the mechanism — a virtual sequencer holding handles to the three agents' sequencers, and virtual sequences that call on all of them. What it does not address is that two of the three cannot be timed against each other.

CURRICULUM-DERIVED, the sequences that actually need cross-domain coordination:

StimulusDomains involvedWhat must be true at the boundary
Bring-up — 6.8sideband, then DDRno host traffic until it completes
Training — Module 21sideband and DDR togetherthe DDR monitor must not treat training as traffic
Power-down entryModule 24sideband, with host traffic in flightevery in-flight request must have drained
Refresh-rate change — Module 15sideband, during trafficthe device-level checker's interval changes mid-run
A mode-register writesideband, during traffic27.6 §8 — derived figures go stale

Row three is the one that cannot be written without a synchronisation point. A power-down entry issued on the sideband while host requests are in flight is a race: the sequence has no way to count DDR cycles, so “wait until the queue drains” cannot be expressed as a delay. It has to be expressed as a condition observed on the DDR side — which means the virtual sequence must consume the DDR-side monitor's output, not merely drive its sequencer.

And rows four and five change a checker's parameters mid-run. §10's TREFI and FAW_WINDOW are elaboration-time parameters; a sequence that changes the device's refresh rate invalidates the bound checker's own constant — which is 27.6 §8's stale-configuration problem arriving from inside the testbench rather than from a VIP.

So a cross-domain sequence needs a declared synchronisation point, and the honest set of them is small: a drain condition observed on the DDR side, a completion response on the host side, or an explicit quiescence window in which no domain is driving. A sequence naming none of these is timing one domain by another's clock, which is §5's error in the stimulus rather than the checking.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ---------------------------------------------------------------------
// ddr_sync_contract -- records how a cross-domain sequence
// synchronises, and refuses one that does not.
//
// CLASSIFICATION: educational, SIMULATION-ONLY.
//
// WHY A DELAY IS REFUSED: §5 and this section. A virtual sequence
// cannot count another domain's cycles, so "wait N cycles then enter
// power-down" is timing one domain by another's clock. It works in
// simulation at one frequency ratio and races at every other.
//
// WHAT IT DOES NOT MODEL:
//   - the sequencer, arbitration, or any UVM sequencing mechanism
//
// WHAT IT WOULD MISS:
//   - a sync point declared and never waited on. This checks the
//     DECLARATION, exactly as §9's topology does.
// ---------------------------------------------------------------------
typedef enum { SYNC_NONE,        // refused
               SYNC_DELAY,       // refused BY NAME -- the plausible error
               SYNC_DDR_DRAIN,   // a condition observed on the DDR side
               SYNC_HOST_RESP,   // a completion observed on the host side
               SYNC_QUIESCENT    // an explicit window with no driver
             } sync_e;

class ddr_sync_contract extends uvm_object;
  `uvm_object_utils(ddr_sync_contract)

  protected sync_e m_sync[string];
  protected string m_domains[string];
  protected bit    m_invalidates_params[string];

  function new(string name = "ddr_sync_contract");
    super.new(name);
  endfunction

  // A sequence spanning ONE domain needs no synchronisation point --
  // that domain's clock orders it. Only cross-domain sequences do.
  function bit declare(string seq, string domains, sync_e s,
                       bit invalidates_params = 0);
    if (seq == "" || domains == "") begin
      `uvm_error("SYNC", "a sequence needs a name and its domains")
      return 0;
    end
    if (s == SYNC_NONE) begin
      `uvm_error("SYNC", $sformatf(
        "'%s' refused: a cross-domain sequence needs a sync point (§12)", seq))
      return 0;
    end
    if (s == SYNC_DELAY) begin
      `uvm_error("SYNC", $sformatf(
        "'%s' refused: a delay times one domain by another's clock (§5)", seq))
      return 0;
    end
    m_sync[seq]                = s;
    m_domains[seq]             = domains;
    m_invalidates_params[seq]  = invalidates_params;
    return 1;
  endfunction

  function int declared(); return m_sync.size(); endfunction

  function int count_with(sync_e s);
    int n = 0;
    foreach (m_sync[k]) if (m_sync[k] == s) n++;
    return n;
  endfunction

  // Rows four and five of this section's table: a sequence that
  // changes the device's configuration invalidates §10's
  // elaboration-time parameters, and the bound checker cannot be
  // reparameterised mid-run. So these must be reported, not hidden.
  function int parameter_invalidating();
    int n = 0;
    foreach (m_invalidates_params[k]) if (m_invalidates_params[k]) n++;
    return n;
  endfunction

  function void report_phase(uvm_phase phase);
    `uvm_info("SYNC", $sformatf(
      "sequences=%0d ddr_drain=%0d host_resp=%0d quiescent=%0d param_invalidating=%0d",
      m_sync.size(), count_with(SYNC_DDR_DRAIN), count_with(SYNC_HOST_RESP),
      count_with(SYNC_QUIESCENT), parameter_invalidating()), UVM_LOW)
    foreach (m_invalidates_params[k])
      if (m_invalidates_params[k])
        `uvm_warning("SYNC", $sformatf(
          "'%s' changes device configuration: §10's bound checker holds stale parameters", k))
  endfunction
endclass

SYNC_DELAY is in the enumeration so it can be refused by name, exactly as §9 refuses ORD_SIM_TIME. It is the plausible answer — a delay is easy to write and works at one frequency ratio — and it races at every other, which is the failure mode that survives a review because it passed a regression.

13. The Three Quantities at This Level

DecisionCoverageCheckingCost
One agent for all interfaces — §1worseworse — no transaction to checkbetter
Three agents — §3betterbetterworse
Omit the sideband agent — §3much worse — no init, training or powerunchangedbetter
Add the device-level checker — §4unchangedmuch better27.4 §6's gapworse
Simulation time for ordering — §5unchangedworse, invisiblybetter
Causal ordering only — §5unchangedweaker and honestworse
Merge the failure vocabularies — §2unchangedworse — a number meaning neitherbetter

Rows five and seven are the two that trade checking for appearance, and they sit where 27.4 §8's fifth row and 27.5 §9's sixth row sat: cheapest, symptomless, and destructive of the thing being measured. That is the third time in this module the pattern has appeared in the same position in the table, and it is the module's most transferable result.

Row three is the one most environments take, and its cost is not subtle once stated: an environment with no sideband agent cannot execute Module 21's training, and cannot command Module 24's power transitions. It can only ever test a device that is already running.

14. Corner Cases

CaseBehaviourWhy
No DDR-side agentTopology refused§9 — 27.3's monitor is the only transaction source
Bank scoreboards, no device-level checkerRefused§4 — 27.4 §6's obligations unowned
No sideband agentRefused§3 — no training or power transition
No ordering policy declaredRefused§5 — the fallback is invisible
ORD_SIM_TIME declaredRefused by name§5 — it orders protocol-simultaneous events
Coverage model, no closure argumentRefused27.5 §11 — a percentage with nothing to qualify it
A refusal fixed by a later declarationClears§9 — sound() recomputes
BANKS = 1Elaboration fails$clog2(1) is 0; the index would be [-1:0]
15 banks in 4 groupsElaboration failsthe geometry is inconsistent
FAW_WINDOW = 1Elaboration failsa one-cycle window cannot hold a history
Five activates in the window, one per bankp_faw_respected fires§4 — the failure sixteen scoreboards miss
A command while powered downp_no_cmd_while_down fires§4 — per-bank state cannot see power
Two events in one domainOrdered by that domain's sequence§5 — the clock provides it free
Two events, different domains, no dependencyORD_UNORDERED§5 — and this is not a failure
Every cross-domain pair orderedordering_is_honest() false§11 — a symptom, not an achievement
A dependency naming an unobserved eventError at report time§11 — a finding about the monitors
relate() on an unnoted eventError, returns unordered§11 — never a silent guess

Rows fourteen and fifteen are the pair to hold together. An unordered pair is the correct answer and an all-ordered run is the wrong one — which is the opposite of how every other count in this module reads, and the reason ordering_is_honest() is named for honesty rather than success.

15. DV — Testing the Architecture

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// SIMULATION-ONLY. Independent reference for §10's window counter: a
// QUEUE of activate timestamps pruned by age, rather than a shift
// register. A different representation, so agreement is evidence.
class faw_reference;
  int unsigned stamps[$];
  int unsigned window;

  function new(int unsigned w);
    window = w;
  endfunction

  function void activate(int unsigned now);
    stamps.push_back(now);
    prune(now);
  endfunction

  // Drop everything older than the window. This is the time
  // information §4 says a per-bank summary discards.
  function void prune(int unsigned now);
    while (stamps.size() > 0 && (now - stamps[0]) >= window)
      stamps.delete(0);
  endfunction

  function int in_window(int unsigned now);
    prune(now);
    return stamps.size();
  endfunction
endclass
CheckWhat it establishes
Replay 100,000 random activates against bothShift register and timestamp queue agree every cycle
Issue FAW_LIMIT activates then one more, inside the windowp_faw_respected fires; both models agree
Issue the same spread one per bank across BANKS banksStill fires — §4's worked failure
Issue them spread just beyond the windowPasses; the window rolled
Elaborate with BANKS = 1$fatal
Elaborate with 15 banks, 4 groups$fatal
Elaborate with FAW_WINDOW = 1$fatal
Hold powered_down and issue any commandp_no_cmd_while_down fires
Assert pd_entry and pd_exit togetherp_pd_edges_exclusive fires
Withhold refresh past the allowancep_refresh_interval fires
Declare a topology with no sideband agentsound() false; the reason names Modules 21 and 24
Declare ORD_SIM_TIMEsound() false; refused by name
Declare it, then declare ORD_CAUSALRefusal clears — sound() recomputes
Relate two same-domain eventsOrdered by sequence; same_domain() rises
Relate two cross-domain events with a dependencyORD_BEFORE; ordered() rises
Relate two cross-domain events without oneORD_UNORDERED; unordered() rises
Run a suite where every pair has a dependencyordering_is_honest() false
Declare a dependency on an unobserved eventdangling_dependencies() rises; error at report

Two reports are worth publishing, because they are the two ways this architecture passes while checking less than it appears to:

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

  (A) sixteen clean scoreboards and a device outside spec
        topology : 3 agents, 16 bank scoreboards, NO device-level
                   checker (declared absent)
        bank scoreboards      16 x  match=2,411,006  mismatch=0
        every read checked, every payload correct
        --------------------------------------------------
        ddr_env_topology.sound()          FALSE
          refused: distributed scoreboards with no device-level
                   checker -- 27.4 §6's cross-bank obligations are
                   unowned

        the stimulus placed five activates inside one 30-cycle
        window, one per bank. Each scoreboard saw ONE activate,
        legal in isolation and not its job anyway. Every payload was
        correct because the device honoured the commands it was
        given. Sixteen clean results, and 14.8's limit violated.

        the only thing that caught it was the TOPOLOGY check, before
        the run -- which is the argument for refusing a topology
        rather than warning about it.

  (B) a correlator that ordered everything
        topology : 3 agents, device-level checker present,
                   order policy ORD_CAUSAL
        events                          4,112,908
        same_domain                     4,096,110
        cross_ordered                      16,798
        cross_unordered                         0
        dangling_deps                           0
        --------------------------------------------------
        ordering_is_honest()            FALSE

        every cross-domain pair was ordered and nothing was left
        ambiguous. On an interface with no shared time base that is
        not a good result -- §5 says most cross-domain pairs have no
        observable order, so a zero here means the dependencies were
        over-declared: the monitors asserted causal links they had
        not observed.

        and because every pair was ordered, 27.4's scoreboards never
        produced a SET. Their exact-comparison count is 100%, which
        reads as full-strength checking and is 27.4 §7's
        independence failure arriving through the correlator.

    diagnosis : (A) is 27.4 §6, unowned for six chapters and caught
      only by a structural refusal. (B) is §5: the honest ordering
      is partial, so a complete one is evidence the partiality was
      papered over -- and it propagates into the scoreboards as
      false exactness.

    the fix : (A) declare the device-level checker; §10's shift
      register is the only structure that can count the window. (B)
      audit which dependencies the monitors actually observed, and
      expect cross_unordered to be LARGE.

16. Debugging

SymptomLikely causeHow to confirm
Sixteen scoreboards clean, device misbehavingNo device-level checker — §4sound(); the topology refusal
A rolling-window violation nobody caughtSamep_faw_respected's cover hit count
Cannot test trainingNo sideband agent — §3agent_mode(IF_SIDEBAND)
Cannot test a power transitionSameModule 24's commands have no path
No ambiguity ever reportedSimulation-time ordering — §5ordering_is_honest() false
Scoreboards always exactFollows from the row above27.4's max_set_size() stuck at 1
Ordering claims nobody can justifyOver-declared dependenciesdangling_dependencies(); audit the monitors
A pass/fail number meaning neither thingMerged failure vocabularies — §2Which domain each failure came from
Host and DDR transactions will not matchNo common transaction identity — §2Module 23 — the mapping is not one-to-one
Cross-domain addresses disagreeNo shared address-space view — §28.6's map and its inversion
The window counter disagrees with a referenceThe stale-register hazard — §10p_count_matches_history
Elaboration fails on geometryThe guards in §10The $fatal message names the parameter

Row eight has no instrument, and it is §2's third row in its failing form. A merged pass/fail number is syntactically fine and semantically empty — nothing in the environment can detect that two failure vocabularies were added together, because the sum is a valid integer.

17. Misconceptions

“A DDR environment is one agent with a wide interface.” §1. A host-side monitor is handed a transaction; a DDR-side monitor must derive one. That is a different component, not a wider one.

“The three interfaces are variations of each other.” §1. They differ in what a transaction is, what a failure is, and what clock they run on — three structural differences, not parameters.

“The sideband agent is optional.” §3. Without it, training and every power-state transition are uncommandable. The environment can only test an already-running device.

“Distributed scoreboards cover the device.” §4. Sixteen clean per-bank results are consistent with a violated rolling activate window, a missed refresh and a command during power-down.

“Centralising the scoreboard fixes the cross-bank gap.” §4, 27.4 §5. It also enlarges every ambiguity set. The fix is a separate component, not a merged one.

“A running total can count a rolling window.” §10. It would need to know what to subtract, which is the time information a summary discards. A shift register is the structure that can.

$realtime is a fine cross-domain time base.” §5. It orders events the protocol defines as simultaneous, so a checker relying on the distinction passes in simulation and describes nothing real.

“A correlator that orders everything is working well.” §11. On interfaces with no shared time base, a zero unordered count means the dependencies were over-declared.

“Cycle counts from two domains are comparable.” §5. Different clocks with no fixed ratio if either is dynamic. The numbers are incomparable, not merely offset.

“One pass/fail number for the environment is a simplification.” §2, §13. It merges failure vocabularies that mean different things, and the result means neither.

“Host requests map one-to-one onto DDR bursts.” §2. Module 23 establishes the mapping is the controller's decision and is not one-to-one in either direction.

18. Interview Reasoning

Why is a DDR environment not one agent? Because the three interfaces differ in what a transaction is. A host request arrives whole; a DDR transaction must be derived from pins, which can fail; a register access is a third protocol.

What do the three interfaces not share? A clock, a transaction identity, a failure vocabulary, and a view of the address space. Every one of those is an assumption a single-agent environment makes silently.

Why does the missing sideband agent matter? Training and power-state transitions are commanded through register accesses. Without that agent the environment can only start from an already-initialised device.

Sixteen per-bank scoreboards all pass. What have you not checked? Every cross-bank obligation: the rolling activate window, refresh, bank-group spacing, power state and cross-bank ordering. Each is device-level and has no per-bank projection.

Why not just centralise the scoreboard? Ambiguity in the expected value is bounded by accesses in flight to one address, and addresses in different banks cannot collide. Centralising enlarges every ambiguity set, so it weakens checking to fix visibility.

How do you count activates in a rolling window? With a shift register over the window. A running total cannot, because it would need to know which activates are leaving — and that is exactly the time information a per-bank summary throws away.

How do you order an event on the host side against one on the DDR side? Only through an observed dependency. There is no common clock, and cycle counts from two clocks are incomparable rather than offset.

Why is simulation time the wrong answer? It gives every pair an order, including pairs the protocol defines as simultaneous. A checker built on it never reports ambiguity and passes in simulation while describing nothing real.

Your correlator reports zero unordered pairs. Good or bad? Bad. Most cross-domain pairs have no observable order, so zero means dependencies were over-declared — and it propagates into the scoreboards as false exactness.

Where would you bind a property set rather than build a class? Anywhere the rule is about pins and timing: the compliance rule checker, the spacing property set, the boundary check, and the device-level cross-bank checker. Everything needing a transaction belongs above the monitor that produces one.

19. Exercises

  1. §7 places fifteen components. Add host-side response checking to the table and state which agent it belongs in and why nothing in this module built it.

  2. §10 uses a shift register of FAW_WINDOW bits. Derive the storage cost for a window of 100 cycles and compare it against the timestamp-queue reference in §15, stating the case where each is preferable.

  3. §5 lists three ordering substitutes. Construct a fourth and show which of the three problems it inherits.

  4. §9 makes six refusals. For each, construct the environment that satisfies it in letter and defeats it in spirit.

  5. §11's ordering_is_honest() requires a non-zero unordered count. Derive the threshold you would actually use on a real campaign, and state what your threshold assumes about the stimulus.

  6. §4's callout argues the rolling window has no per-bank projection. Prove it, or find the obligation in §4's table that does have one.

  7. §13's rows five and seven both trade checking for appearance. Find the equivalent row in 27.4 §8 and 27.5 §9, and state the property all four share.

  8. A colleague proposes one scoreboard consuming both host and DDR transactions with $realtime for ordering. Identify every argument in this chapter it violates, then say what it would still catch.

20. Where This Goes

A DDR verification environment is three agents, sixteen per-bank scoreboards, a device-level checker, a correlator, and a closure argument that refuses to reduce them to one number. The three interfaces are not variations of each other; they share no clock, no transaction identity, and no notion of what a failure is. The cross-bank obligations that a distributed scoreboard leaves unowned need a component with the raw stream and a shift register, not a merge of sixteen summaries. And there is no cross-domain time base, so the honest ordering is partial and a complete one is evidence of over-claiming.

Four results carry forward. The sideband agent is not optional — without it an environment can only test an already-running device. A shift register, not a running total, because a rolling window needs to know what is leaving it. The honest count is the one that fails to order things, which inverts how every other number in this module reads. And rows five and seven of §13 are the third appearance in Module 27 of the same pattern: the cheapest option, symptomless, destroying the thing being measured — after 27.4 §8's controller-derived ordering and 27.5 §9's missing reachability model.

Two things stay open, and both are honest limits rather than omissions. Host-side response checking was not built hereModule 23 owns the controller's obligations to its requester, and §7's table says so rather than implying coverage. And a merged failure vocabulary has no instrument, because the sum of two meanings is a valid integer.

Module 27 as a whole has one result. Every chapter built an instrument, and in every chapter the instrument's most important output was a report of its own weakness: a residual that refuses a percentage, an antecedent count that exposes vacuity, a resolution flag that admits ignorance, a legal set whose size measures lost strength, a denominator that must be reviewed, a capability register where unknown stays unknown, and an ordering that reports what it could not order. A verification environment that cannot report how little it checked is not a weaker version of one that can. It is a different thing, and it passes.

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.