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 side | DDR side | Sideband | |
|---|---|---|---|
| What arrives | a request — address, size, direction | pins — command, address, strobes, data | a register access |
| Protocol | the SoC's fabric protocol | DDR commands and timing | a different protocol entirely |
| Clock | the fabric clock | the DDR clock — 6.1 | its own, typically far slower |
| A transaction is | given to you, whole | reconstructed — 27.3 | a single read or write |
| Ordering | explicit, and part of the contract | the controller's choice — Module 23 | none worth modelling |
| A failure is | a wrong response or a lost request | a timing violation or illegal command | a wrong register value |
| Data correctness | checkable — 27.4 | checkable at the pins | not 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.
| Assumption | Where it fails | What breaks if you assume it anyway |
|---|---|---|
| One clock | 6.1 | any ordering comparison across domains — §5 |
| One transaction identity | Module 23 | request-to-burst matching; every scoreboard |
| One failure vocabulary | §1 row six | a pass/fail number that means neither thing |
| One address space view | 8.6 | host 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.
| Agent | Mode | Its monitor | Contains |
|---|---|---|---|
| Host | active | conventional | driver, sequencer, monitor |
| DDR | passive (controller DUT) or active (device DUT) | 27.3's | monitor always; driver only when active |
| Sideband | active | conventional | register 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:
| Obligation | Owner | Why it is device-level |
|---|---|---|
| Rolling activate window — 14.8 | the device | it counts activates in any bank |
| Refresh — Module 15 | the device | a refresh command addresses all banks |
| Bank-group spacing — 5.3 | pairs of banks | the relation is between two bank models |
| Power state — 6.2 | the device | per-bank state says nothing about it |
| Cross-bank ordering | the controller | each 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:
| Substitute | How it works | What it gets wrong |
|---|---|---|
| Simulation time | compare $realtime | correct, and finer than either protocol's resolution — two events in one DDR cycle get an order the protocol does not define |
| Domain cycle counts | compare cycle numbers | incomparable — different clocks, no fixed ratio if either is dynamic |
| Causality only | order only events with an observed dependency | correct 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.
| Phase | Active | Why this order |
|---|---|---|
| 1 | DDR-side monitor only — passive | 27.3 §6 — it must see the first command |
| 2 | Sideband — bring-up | 6.8 — reset release, before the device is addressable |
| 3 | Sideband — training | Module 21 — and the monitor must not score it |
| 4 | Host — traffic | the only phase where 27.4's scoreboards mean anything |
| 5 | Sideband, concurrently | mode-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.
| Component | Chapter | Where it goes | Why there |
|---|---|---|---|
| Compliance registry | 27.1 §9 | environment level | the obligation is device-wide |
| Bindable rule checker | 27.1 §11 | bound at the DDR interface | it checks pins |
| Spacing property set | 27.2 §11 | bound at the DDR interface | same |
| Vacuity harness | 27.2 §13 | environment level | it reports across all properties |
| DDR monitor | 27.3 §11 | DDR-side agent | it derives transactions from pins |
| Reference memory | 27.4 §10 | one per bank | the address space partitions — 5.2 |
| Order resolver | 27.4 §11 | one per bank | ambiguity is bounded within a bank |
| Bank scoreboard | 27.4 §12 | one per bank, sixteen of them | 27.4 §5 |
| Device-level checker | §10 here | environment level | 27.4 §6's gap |
| Coverage model | 27.5 §12 | environment level, three groups | 27.5 §12 — three sampling events |
| Closure argument | 27.5 §11 | environment level | it consumes every model's counts |
| Capability registry | 27.6 §10 | environment level | it describes the whole integration |
| Boundary property set | 27.6 §11 | bound at the VIP boundary | it checks the integration's assumptions |
| Cross-check | 27.6 §12 | DDR-side agent | it compares two monitors of one interface |
| Cross-domain correlator | §11 here | environment 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 it — Module 23 owns the controller's obligations to its requester, and that is where it belongs.
8. The Architecture
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
// ---------------------------------------------------------------------
// 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
endclassORD_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
// ---------------------------------------------------------------------
// 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));
endmoduleA 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
// ---------------------------------------------------------------------
// 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
endclassordering_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:
| Stimulus | Domains involved | What must be true at the boundary |
|---|---|---|
| Bring-up — 6.8 | sideband, then DDR | no host traffic until it completes |
| Training — Module 21 | sideband and DDR together | the DDR monitor must not treat training as traffic |
| Power-down entry — Module 24 | sideband, with host traffic in flight | every in-flight request must have drained |
| Refresh-rate change — Module 15 | sideband, during traffic | the device-level checker's interval changes mid-run |
| A mode-register write | sideband, during traffic | 27.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.
// ---------------------------------------------------------------------
// 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
endclassSYNC_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
| Decision | Coverage | Checking | Cost |
|---|---|---|---|
| One agent for all interfaces — §1 | worse | worse — no transaction to check | better |
| Three agents — §3 | better | better | worse |
| Omit the sideband agent — §3 | much worse — no init, training or power | unchanged | better |
| Add the device-level checker — §4 | unchanged | much better — 27.4 §6's gap | worse |
| Simulation time for ordering — §5 | unchanged | worse, invisibly | better |
| Causal ordering only — §5 | unchanged | weaker and honest | worse |
| Merge the failure vocabularies — §2 | unchanged | worse — a number meaning neither | better |
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
| Case | Behaviour | Why |
|---|---|---|
| No DDR-side agent | Topology refused | §9 — 27.3's monitor is the only transaction source |
| Bank scoreboards, no device-level checker | Refused | §4 — 27.4 §6's obligations unowned |
| No sideband agent | Refused | §3 — no training or power transition |
| No ordering policy declared | Refused | §5 — the fallback is invisible |
ORD_SIM_TIME declared | Refused by name | §5 — it orders protocol-simultaneous events |
| Coverage model, no closure argument | Refused | 27.5 §11 — a percentage with nothing to qualify it |
| A refusal fixed by a later declaration | Clears | §9 — sound() recomputes |
BANKS = 1 | Elaboration fails | $clog2(1) is 0; the index would be [-1:0] |
| 15 banks in 4 groups | Elaboration fails | the geometry is inconsistent |
FAW_WINDOW = 1 | Elaboration fails | a one-cycle window cannot hold a history |
| Five activates in the window, one per bank | p_faw_respected fires | §4 — the failure sixteen scoreboards miss |
| A command while powered down | p_no_cmd_while_down fires | §4 — per-bank state cannot see power |
| Two events in one domain | Ordered by that domain's sequence | §5 — the clock provides it free |
| Two events, different domains, no dependency | ORD_UNORDERED | §5 — and this is not a failure |
| Every cross-domain pair ordered | ordering_is_honest() false | §11 — a symptom, not an achievement |
| A dependency naming an unobserved event | Error at report time | §11 — a finding about the monitors |
relate() on an unnoted event | Error, 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
// 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| Check | What it establishes |
|---|---|
| Replay 100,000 random activates against both | Shift register and timestamp queue agree every cycle |
Issue FAW_LIMIT activates then one more, inside the window | p_faw_respected fires; both models agree |
Issue the same spread one per bank across BANKS banks | Still fires — §4's worked failure |
| Issue them spread just beyond the window | Passes; 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 command | p_no_cmd_while_down fires |
Assert pd_entry and pd_exit together | p_pd_edges_exclusive fires |
| Withhold refresh past the allowance | p_refresh_interval fires |
| Declare a topology with no sideband agent | sound() false; the reason names Modules 21 and 24 |
Declare ORD_SIM_TIME | sound() false; refused by name |
Declare it, then declare ORD_CAUSAL | Refusal clears — sound() recomputes |
| Relate two same-domain events | Ordered by sequence; same_domain() rises |
| Relate two cross-domain events with a dependency | ORD_BEFORE; ordered() rises |
| Relate two cross-domain events without one | ORD_UNORDERED; unordered() rises |
| Run a suite where every pair has a dependency | ordering_is_honest() false |
| Declare a dependency on an unobserved event | dangling_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:
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
| Symptom | Likely cause | How to confirm |
|---|---|---|
| Sixteen scoreboards clean, device misbehaving | No device-level checker — §4 | sound(); the topology refusal |
| A rolling-window violation nobody caught | Same | p_faw_respected's cover hit count |
| Cannot test training | No sideband agent — §3 | agent_mode(IF_SIDEBAND) |
| Cannot test a power transition | Same | Module 24's commands have no path |
| No ambiguity ever reported | Simulation-time ordering — §5 | ordering_is_honest() false |
| Scoreboards always exact | Follows from the row above | 27.4's max_set_size() stuck at 1 |
| Ordering claims nobody can justify | Over-declared dependencies | dangling_dependencies(); audit the monitors |
| A pass/fail number meaning neither thing | Merged failure vocabularies — §2 | Which domain each failure came from |
| Host and DDR transactions will not match | No common transaction identity — §2 | Module 23 — the mapping is not one-to-one |
| Cross-domain addresses disagree | No shared address-space view — §2 | 8.6's map and its inversion |
| The window counter disagrees with a reference | The stale-register hazard — §10 | p_count_matches_history |
| Elaboration fails on geometry | The guards in §10 | The $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
-
§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.
-
§10 uses a shift register of
FAW_WINDOWbits. 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. -
§5 lists three ordering substitutes. Construct a fourth and show which of the three problems it inherits.
-
§9 makes six refusals. For each, construct the environment that satisfies it in letter and defeats it in spirit.
-
§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. -
§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.
-
§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.
-
A colleague proposes one scoreboard consuming both host and DDR transactions with
$realtimefor 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 here — Module 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
Related tutorials
- Related topic
Senior Verification Strategy
Use UVM, add assertions, add coverage names three tools and zero obligations. Start from what must be true, then ask what evidence each kind of truth admits — and hold the eight reasons a green assertion proves nothing.
- Related topic
PHY Responsibilities
The controller decides which DDR operation should happen. The PHY makes it real at a pin boundary whose timing the controller cannot meet — and the line between them is not the same in any two implementations.
- Related topic
JEDEC Compliance
A compliance claim has a structure: an enumerated rule list, a result per rule, and a residual. This chapter builds the structure — and refuses to invent the rule list, because JEDEC would not serve it.
- Related topic
DDR Assertions
662 of this curriculum's 843 named properties are implications, so 78.53% can pass by never arming. Built on the measured census, not on advice.
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.
