Skip to content
VLSI Mentor

DDR · Module 31

DDR vs SRAM

SRAM is not faster DRAM: its read is non-destructive, and that one structural difference creates or deletes thirteen controller obligations. The decision has two stages, and admissibility must be settled before capacity.

The comparison everybody makes is the wrong one. SRAM is fast and small, DRAM is slow and big, pick according to size is a summary of two datasheets, and it is not a decision procedure.

The decision has two stages and they are asked in a fixed order.

First: is this technology ADMISSIBLE for this block — can the requester tolerate the access contract it offers? Second, and only then: is it AFFORDABLE at the required capacity?

Most engineers run only the second stage. The first is where the expensive mistakes live, because a block that cannot tolerate a variable completion time will not be rescued by any amount of capacity.

And the reason admissibility comes first is a difference in kind, not degree. A static cell's read leaves the cell holding its value. A DRAM cell's read destroys it. CURRICULUM-DERIVED from 1.3 §1 and 2.5: everything the rest of this curriculum builds — precharge, activate, row state, restore, refresh, the timing-legality framework, the scheduler — exists to manage the consequences of that one property. Choose the non-destructive cell and the entire apparatus is not optimised away; it was never required.

So this chapter's claim is that the choice is not a point on an axis. It is a choice between two contracts, and the contract difference is visible as logic that exists or does not exist.

1. The Decision Has Two Stages

Stage one — admissibility. Can the requester tolerate this technology's access contract? The contract is not a latency number. It is the answer to four questions:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   the ACCESS CONTRACT, four questions

   C1  Is the completion time a CONSTANT, a BOUND, or neither?
   C2  Does an access depend on the HISTORY of previous accesses?
   C3  Can the memory REFUSE or DEFER an access for its own reasons?
   C4  Does the requester have to participate in MAINTENANCE?

Stage two — affordability. At the capacity this block needs, which technology costs less in the currency the project is actually paying — area, power, package, or money? Chapter 1.6 owns that currency question and its trade surface; this chapter takes its result and uses it as stage two.

The order is not interchangeable, and that is the chapter's first result.

STRUCTURAL: a block whose correctness depends on a fixed completion time is inadmissible for DRAM at any capacity. No amount of capacity advantage rescues it, because the failure is not a performance shortfall — it is a contract violation, and §12 shows exactly what it looks like in hardware.

Whereas the reverse direction is merely expensive. A block that could tolerate DRAM but is given SRAM works correctly and costs too much. So the two errors are not symmetric: running stage two first produces a correctness bug, and running stage one conservatively produces a cost overrun. An engineer who knows which of their two possible mistakes is recoverable has the order right.

2. The Seven Axes

Every chapter in Module 31 compares along the same seven axes. That is the point of having them: four chapters that each invent their own framing cannot be laid side by side, and a comparison you cannot lay side by side is a set of essays.

AxisThe question it asksWhy it decides something
A1 Cell mechanismdoes a read destroy the stored value?it creates or deletes A2 entirely — §4
A2 State obligationwhat must the controller remember between accesses?it sets the controller's size, its verification surface, and its bug population
A3 Access granularitywhat is the smallest useful transfer?it sets payload efficiency — 12.4
A4 Channel shapewidth × count, and what is independent?it sets address decode and scheduler replication cost — 31.3
A5 Latency compositionwhich terms make up an access, and are they constant?it answers C1 and C2, so it decides admissibility
A6 Bandwidth rungwhich of the five rungs does this technology move?stops peak numbers being compared — 30.8 §2
A7 Physical couplingon-die, on-package, soldered, or socketed?it decides what becomes impossible: repair, upgrade, test access, capacity change

A1 is first because it is causal rather than merely descriptive. The other six are consequences of it to varying degrees, and in this chapter's comparison A1 alone explains A2, A3 and A5.

A7 is last because it is the axis engineers forget and the one that is irreversible. A choice that soldered the memory down cannot be revisited after tape-out, and 31.3 §16 and 31.4 both turn on it.

3. What Is Actually the Same

The most common failure in comparison writing is over-differentiating, so this section comes before any difference.

Both technologies are volatile. CURRICULUM-DERIVED from 1.3 §1, which fixes the distinction precisely: static is a claim about maintenance, not about persistence. Remove the supply and both lose their contents. So “static” does not mean “non-volatile”, and 1.5 owns the tier that genuinely remembers without power.

Both share their periphery, and for the same reason. CURRICULUM-DERIVED from 1.3 §3's amortisation argument, which it states as a principle governing DRAM too: the expensive parts of a memory are built once and amortised over many cells, and the cell is made as small as possible because it is the part that is replicated. Row decoder shared across columns, sense amplifiers shared across rows, control shared across the array.

Both are therefore exclusive in the same way. One wordline at a time means one row at a time. Chapter 1.3 §3 states the consequence: a structure with shared periphery serves one access per port per cycle, and a single-ported array serves one access, full stop. Bank-level parallelism in DDR is a response to that same exclusivity — not an escape from it.

Both are addressed by decoding an address into a row selection. The field split, the inversion, and the performance consequences are Module 18's, but the shape is shared.

And a controller contains both. Chapter 1.3 §8 makes the observation that is worth carrying into every later module: the DDR controller of Module 17 is full of on-chip arrays — its command queues, its reorder buffers, its trained-value registers. A memory controller contains memory, so this is not a comparison between two kinds of chip. It is a comparison an engineer makes repeatedly, inside one design, per block. §15 is that observation developed.

4. A1 — The Difference in Kind

One sentence, and everything else is downstream of it.

A static read resolves a difference the cell is actively producing. A dynamic read destroys the thing it measured.

CURRICULUM-DERIVED from 1.3 §4: SRAM is fast because nothing has to be rebuilt — sensing resolves a difference the cell drives, no part of the access restores anything afterwards, there is no maintenance operation competing for the array, and an SRAM access is architecturally a single operation.

CURRICULUM-DERIVED from 2.5, which owns the read interaction end to end as five stages and whose §6 is titled The Access Cannot End Here: the sense operation is amplification, not detection, and the cell's charge has been shared away, so a restore is not an optimisation — it is part of the access. Chapter 2.6 owns the restore itself.

Now derive the consequences rather than listing them. Each arrow below is a because, not an and:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   A1  read destroys the stored value
        |
        +-> the value must be REBUILT after every read
        |     -> the rebuild takes time the requester did not ask for
        |     -> the rebuilt row is now HELD somewhere (the row buffer)
        |          -> so an access's cost DEPENDS ON WHAT IS HELD  (C2)
        |          -> so completion time is not a constant          (C1)
        |          -> so "which row is open" becomes CONTROLLER STATE (A2)
        |
        +-> charge leaks even when NOT read
              -> periodic rewrite is MANDATORY
                   -> the memory needs the bus for its own purposes (C3)
                   -> and somebody must own the schedule for that   (C4)

Read the right-hand column. All four contract questions of §1 are answered by A1 alone, and all four answers come out the hard way for DRAM and the easy way for SRAM. That is what “difference in kind” means, and it is why §1 insists admissibility is checked before capacity.

The inversion worth stating, because it is the useful form: SRAM does not lack a row buffer because it is simpler. It lacks one because there is nothing to hold — the cell still has its value, so there is no rebuilt copy needing a home. A design that adds a “row buffer” to an SRAM has added a cache, which is a different structure answering a different question.

5. The Obligation Set, Counted

A2 is the axis that shows up in the design, so count it rather than describe it.

Each row below is controller state that exists because of A1, with the chapter that owns it. STRUCTURAL — the list is a property of the access contract, not of a generation.

ObligationController state it requiresExists for DDRFor SRAMOwner
Row state per bankopen/closed + open row index + a third value for unknownyesno7.4, 30.2
Precharge before a different rowper-bank elapsed countersyesno14.2
Activate-to-column separationper-bank elapsed countersyesno14.1
Minimum row-open timeper-bank elapsed countersyesno14.3
Column-to-column spacing, scopedper-group elapsed countersyesno14.6
Activate rate limitinga rolling window of activate timestampsyesno14.8
Refresh accruala credit ledger with postponement and pull-inyesno15.3
Refresh executiona manager with a drain phase and an occupancy gateyesno17.3
Read-data return predictiona latency pipeline the controller must predictyesfixed, so trivial10.2
Write-data delivery deadlinea pipeline that must meet an offsetyesno11.2
Direction turnarounda shared-bus direction costyesport-dependent30.5 §7
Legality evaluationa maximum over every applicable rule, every cycleyesno13.3
Scheduling among legal candidatesqueues, policy layers, a fairness boundyesarbitration only17.1, 17.4

Thirteen obligations. Twelve of them are absent in the non-destructive case, and the thirteenth degenerates to plain arbitration.

DERIVED, and this is the number worth carrying: the destructive-read property is responsible for essentially the entire state-holding content of a DDR controller. Not most of its logic — its logic also includes address mapping and the host interface — but nearly all of the state it must keep coherent with a device it cannot observe, which is where its bugs live.

Two things this table licenses, and one it does not.

It licenses a verification-surface argument. Every row is state that can go stale, be classified against the wrong reference, or be sized for an index when it needed a count — the hazards 30.2 and 30.3 catalogue. So choosing the destructive-read contract is choosing a bug population, and that is a legitimate input to a design review.

It licenses an area argument that is not about cells. The controller is silicon too. CURRICULUM-DERIVED from 1.3 §8 — the controller's own queues are on-chip arrays — so a fair comparison charges DRAM for its controller and SRAM for its absence. §7 puts that term in the arithmetic explicitly, because it is the term naive comparisons drop.

It does not license “SRAM is simpler” as a conclusion. Chapter 1.3 §6 owns the single-port exclusivity cost, and multi-porting an SRAM is its own hard problem. The obligations are fewer; they are not zero, and they are of a different kind.

6. A5 — Latency Composition, Measured in the Same Domain

Axis A3 corollary from §2, and the rule that makes this section necessary: if you measure one side at the array and the other at the pins, you have compared nothing.

CURRICULUM-DERIVED from 10.5 §1, which owns the three timing domains and the discipline that prevents an entire class of error: before comparing two times, state which domain each was measured in and what unit it is in.

So state the domain for both sides, and use the same one. Domain: from the requester's access request to the first byte usable by the requester.

TermSRAM, on-dieDDR, off-package
Requester to memory interfaceshort on-die pathinterconnect transit — 29.1
Queueingarbitration only, if sharedthe largest term under load30.4 §4
Row-state worknone — A10, or activate, or precharge then activate
Array accessone operation — 1.3 §4column access, then the configured latency
Transferwidth-dependentburst, BL/2 clock cycles — 30.4 §2
Return pathon-diePHY plus interconnect return
Variability across accessesessentially nonethree outcomes, different costs30.2

The last row is the admissibility answer and it is worth more than every row above it.

DERIVED from a stated model, ILLUSTRATIVE, purely to show the shape of the variability rather than any real figure. Take a DDR access whose row-hit path costs H, whose miss path costs H + a, and whose conflict path costs H + p + a, where a is the activate-to-column separation and p the precharge time:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   ILLUSTRATIVE H = 20, a = 14, p = 14 (units: clock cycles; the real
   values are per-device and per-speed-bin, and 14.1/14.2 own them)

     row hit       20
     row miss      34      = 20 + 14        -> 1.70x the hit
     row conflict  48      = 20 + 14 + 14   -> 2.40x the hit

   DERIVED spread: the SAME access costs between 20 and 48 depending
   only on what previous traffic left behind. Ratio 2.40.

   the equivalent SRAM figure is ONE number, because C2's answer is
   "no" -- there is no history for the cost to depend on.

So the deciding quantity for admissibility is not a latency. It is a ratio — the spread between the best and worst completion time — and the question is whether the requester's correctness or its real-time bound can absorb it. A requester that can is admissible; one that cannot is not, at any capacity.

And the honest qualification, which a strong engineer volunteers: those three outcomes are not the whole spread. Queueing, refresh occupancy and direction turnaround widen it further, and 23.1 owns the component with no upper bound at all. So the real spread is worse than 2.40 and the bound must be argued rather than measured — which is precisely why 23.1 holds that the average is the wrong statistic.

7. The Deciding Quantities, With Arithmetic

Two stages, two quantities. Both are stated parametrically because 1.3 §4 and 1.6 are explicit that the constants are process- and variant-dependent, and a chapter that invented them would be teaching a number instead of a method.

Stage one — admissibility. The quantity is the tolerable spread.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   ADMISSIBLE(block, technology)  iff

       worst_case_completion(technology)  <=  deadline(block)

   and note what is NOT in that inequality: capacity, cost, area,
   bandwidth. Stage one is a pure feasibility test and it either
   passes or it does not.

   for a destructive-read technology the left-hand side must be
   ARGUED from the obligation set of §5, because §6's component with
   no upper bound means it cannot simply be measured.

Stage two — affordability. The quantity is total cost at the required capacity, and the term everyone drops is the controller.

DERIVED, from a stated model. Let N be the required capacity in bits, A_s and A_d the per-bit area of the static and dynamic cells in the same process, P_s and P_d their shared-periphery areas, and K the area of the controller the dynamic contract requires but the static one does not:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   static   total_area  =  N * A_s  +  P_s
   dynamic  total_area  =  N * A_d  +  P_d  +  K

   the crossover capacity N* is where they are equal:

       N* * A_s + P_s  =  N* * A_d + P_d + K

       N*  =  ( P_d + K - P_s )  /  ( A_s - A_d )

   STRUCTURAL, and read the form rather than any value:

   - A_s > A_d, so the denominator is POSITIVE -- 1.3 §4's argument
     that several actively driven transistors cannot be as small as
     the minimum possible number of devices.
   - the numerator is dominated by K for small systems, because a
     controller has a FLOOR: §5's thirteen obligations do not get
     cheaper when the array gets smaller.
   - so N* is NOT near zero. below N* the static contract wins on
     cost as well as on admissibility, and that is why small
     on-chip memories are never dynamic.

Three results follow from the form of that expression, and none of them needs a number.

The controller term K sets a floor, and the floor is why the crossover is not near zero. A dynamic memory too small to amortise its own controller is strictly worse on every axis — worse latency, worse variability, worse area, same volatility. That is the whole reason axis A7 and this chapter's §15 talk about per-block decisions.

K also does not scale down, which inverts a common intuition. Halving the capacity halves N * A_d and changes K hardly at all, so the dynamic contract gets relatively more expensive as the instance gets smaller. An engineer proposing a small DRAM for an on-chip buffer has usually compared A_s against A_d and forgotten the other two terms.

And above N* the static contract loses super-linearly in the currency that is usually binding. Chapter 1.3 §4 names it: an SRAM array consumes power in proportion to how much storage exists rather than how much of it is in use, because its mechanism is maintenance by continuous drive. So at large N the static option fails a power budget before it fails an area budget, and a comparison run only on area will pick it and then be redesigned.

The follow-up worth answering before it is asked: what moves N*? A process change moves A_s and A_d; an on-package integration moves P_d; and a controller you already have moves K to nearly zero. That last one is the real-world answer — a chip that already contains a DDR controller for its main memory pays almost no marginal K to serve another block from it, which is why the crossover for a new block in an existing SoC sits far lower than the crossover for a standalone design. §15 is that case.

8. Two Contracts, as a Stack

A stacked layer diagram comparing the two access contracts from the requester downwards. The top layer is the requester, which issues an address and expects data, and is identical in both cases. The second layer is the access contract itself, and this is where the two technologies diverge: a static memory offers a constant completion time with no dependence on access history, while a dynamic memory offers a completion time that depends on which row is currently open and can be deferred for the memory's own maintenance. The third layer, present only for the dynamic contract, is the obligation set: thirteen items of controller state including per-bank row state, elapsed-time counters for every timing class, a rolling activate window, a refresh credit ledger and a refresh manager with a drain phase. The fourth layer, again present only for the dynamic contract, is legality evaluation, which computes the earliest legal issue time as a maximum over every applicable rule on every cycle. The fifth layer is scheduling, which for the dynamic contract chooses among legal candidates under a fairness bound and for the static contract degenerates to plain arbitration between ports. The sixth layer is the array access itself, which for the static contract is architecturally one operation that leaves the cell holding its value, and for the dynamic contract is a sense operation that destroys the stored value and must be followed by a restore. The bottom layer is the cell, and it is the causal layer: the non-destructive bistable cell requires none of layers three, four or five, while the destructive single-transistor cell requires all of them. The diagram is read bottom-up as a causal chain rather than top-down as a stack, because the cell property creates the obligations rather than the obligations constraining the cell.The same request, two contracts — and the layers only one of them needsRequester — address in, data outIDENTICAL for both contracts. This is the layer that must tolerate what is below it.IDENTICAL for both contracts. This is the layer that must tolerate what is below it.The access contract — C1 to C4 (§1)Static: constant, history-free, never deferred. Dynamic: history-dependent, deferrable, variable.Static: constant, history-free, never deferred. Dynamic: history-dependent, deferrable, variable.Obligation set — DYNAMIC ONLY§5's thirteen items: row state, elapsed counters, rolling window, refresh ledger, drain manager.§5's thirteen items: row state, elapsed counters, rolling window, refresh ledger, drain manager.Legality evaluation — DYNAMIC ONLYEarliest legal issue = a maximum over every applicable rule, recomputed every cycle (13.3).Earliest legal issue = a maximum over every applicable rule, recomputed every cycle (13.3).SchedulingDynamic: choose among legal candidates with a fairness bound. Static: plain port arbitration.Dynamic: choose among legal candidates with a fairness bound. Static: plain port arbitration.Array accessStatic: one operation, cell keeps its value. Dynamic: sense destroys it, restore is part of the access.Static: one operation, cell keeps its value. Dynamic: sense destroys it, restore is part of the access.The cell — THE CAUSAL LAYER (A1)Non-destructive bistable: needs none of the three layers above it. Destructive 1T1C: needs all of them.Non-destructive bistable: needs none of the three layers above it. Destructive 1T1C: needs all of them.

Read it bottom-up. The bottom layer is not the foundation of a stack — it is the cause of the three highlighted layers. Two of them exist for one contract and not the other, and the third degenerates.

And read the top layer twice. The requester is identical in both cases, which is the entire reason §1's admissibility test is meaningful: the same block, unchanged, is admissible under one contract and not the other. §9 builds exactly that — one frontend, one requester interface, two contracts.

9. RTL — One Frontend, Two Contracts

The signature artifact of this module. One requester interface, one module, and a single parameter that selects the access contract. The generate boundaries are the comparison: what elaborates under one setting and not the other is §5's obligation set, in synthesisable form.

Read the parameter list first, and notice that the parameters are not independent. NEEDS_REFRESH defaults from DESTRUCTIVE_READ because §4's causal chain is not optional — a contract with a destructive read and no refresh obligation does not exist, and §13's audit block exists to enforce that.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ---------------------------------------------------------------------
// mem_access_frontend -- the parameterised comparative block of §9.
//
// CLASSIFICATION: synthesisable, ILLUSTRATIVE parameter values, and
// CORRECT as written. The intentionally defective block is §11.
//
// WHAT IT IS: one requester-facing frontend that instantiates EITHER
// access contract of §1 under a single causal parameter. The generate
// boundaries below are §5's obligation table expressed as hardware:
// everything inside `if (DESTRUCTIVE_READ)` exists BECAUSE a read
// destroys the stored value (§4), and nothing inside it is an
// optimisation that the static contract merely does without.
//
// WHY IT EXISTS HERE: §5 counts thirteen obligations in prose. Prose
// can be argued with. A generate boundary cannot: the logic either
// elaborates or it does not, and `obligation_count` reports which.
//
// HOW TO RUN IT: elaborate twice, once per contract, and compare the
// synthesised state-element count and `obligation_count`.
// EXPECTED RESULT: the static configuration instantiates no row
// state, no elapsed counters, no rolling window and no refresh
// ledger, and reports obligation_count = 0.
//
// SYNTHESIS: the dynamic configuration is dominated by per-bank
// counters -- BANKS * (3 counters + a row index + 2 state bits).
// The static configuration is a registered address path.
//
// LIMITATIONS: this is a LEGALITY and CONTRACT frontend, not a
// scheduler. It answers "may this access proceed, and when will it
// complete", and deliberately does NOT choose among candidates --
// 17.1 and 17.4 own that, and 30.5 owns why the two must not merge.
// It models one outstanding access so that the contract, rather than
// concurrency, is what the reader compares. §16 states what changes
// when that assumption is relaxed.
// ---------------------------------------------------------------------
module mem_access_frontend #(
  // ---- A1: the causal parameter. Everything else follows. --------
  parameter bit DESTRUCTIVE_READ = 1'b1,

  // ---- Derived, NOT independent. §4's chain forbids the other
  //      combination, and §13 audits it rather than trusting it.
  parameter bit NEEDS_REFRESH    = DESTRUCTIVE_READ,

  // ---- Geometry. Meaningful only for the dynamic contract. -------
  parameter int BANKS            = 16,
  parameter int ROW_W            = 16,

  // ---- ILLUSTRATIVE timing. Real values are per-device and
  //      per-speed-bin; 14.1 to 14.3 own them and this block quotes
  //      none of them as fact.
  parameter int TRCD             = 14,
  parameter int TRP              = 14,
  parameter int TRAS             = 34,

  // ---- The static contract's single completion figure. -----------
  parameter int FIXED_LAT        = 3,

  // ---- COUNT, not INDEX. These counters must REPRESENT their
  //      thresholds, so each needs $clog2(threshold + 1) bits. Sized
  //      $clog2(threshold) a counter saturates one short of the
  //      value it is compared against, and the comparison becomes
  //      unreachable -- the guard is then present in the source and
  //      absent in the silicon.
  parameter int CNT_W            = $clog2((TRAS > TRP ? (TRAS > TRCD ? TRAS : TRCD)
                                                      : (TRP  > TRCD ? TRP  : TRCD)) + 1),
  parameter int BANK_W           = $clog2(BANKS)
)(
  input  logic               clk,
  input  logic               rst_n,

  // ---- Requester side. IDENTICAL for both contracts -- §8's top
  //      layer, and the reason §1's admissibility test is meaningful.
  input  logic               req_valid,
  input  logic [BANK_W-1:0]  req_bank,
  input  logic [ROW_W-1:0]   req_row,
  output logic               req_ready,

  // ---- Device side.
  output logic               cmd_pre,
  output logic               cmd_act,
  output logic               cmd_col,
  output logic [BANK_W-1:0]  cmd_bank,

  // ---- Maintenance. Only the dynamic contract drives this.
  input  logic               refresh_due,
  output logic               refresh_req,

  // ---- Contract reporting, for §13's audit and for the testbench.
  output logic [2:0]         access_outcome,
  output logic [7:0]         obligation_count
);
  // Outcome encoding, shared by both contracts so one scoreboard
  // serves both. OUT_UNKNOWN is a FINDING, not a fourth path --
  // 7.4's three-valued bank-state discipline, and 30.2 §8's point
  // that reporting a confident wrong row is worse than reporting
  // nothing.
  localparam logic [2:0] OUT_IDLE     = 3'd0;
  localparam logic [2:0] OUT_STATIC   = 3'd1;  // non-destructive path
  localparam logic [2:0] OUT_HIT      = 3'd2;
  localparam logic [2:0] OUT_MISS     = 3'd3;
  localparam logic [2:0] OUT_CONFLICT = 3'd4;
  localparam logic [2:0] OUT_UNKNOWN  = 3'd5;

  initial begin
    if (BANKS < 1)
      $fatal(1, "mem_access_frontend: BANKS must be >= 1 (got %0d)", BANKS);
    if (FIXED_LAT < 1)
      $fatal(1, "mem_access_frontend: FIXED_LAT must be >= 1");
    // §4's causal chain, enforced at elaboration. A destructive read
    // without a refresh obligation is not a contract that exists, and
    // a parameterised comparative block that permits the combination
    // can be configured into a memory technology nobody sells.
    if (DESTRUCTIVE_READ && !NEEDS_REFRESH)
      $fatal(1, "mem_access_frontend: DESTRUCTIVE_READ implies NEEDS_REFRESH (§4)");
    // The reverse is also incoherent: charge that does not leak away
    // on a read has no reason to leak away while idle.
    if (!DESTRUCTIVE_READ && NEEDS_REFRESH)
      $fatal(1, "mem_access_frontend: NEEDS_REFRESH without DESTRUCTIVE_READ is incoherent (§4)");
    if (DESTRUCTIVE_READ && (TRAS < TRCD))
      $fatal(1, "mem_access_frontend: TRAS < TRCD leaves no legal column window");
  end

  generate
  // ===================================================================
  // THE DESTRUCTIVE CONTRACT. Everything here exists because of §4.
  // ===================================================================
  if (DESTRUCTIVE_READ) begin : g_dynamic

    // ---- Obligation 1: row state, three-valued. ---------------------
    logic             row_open  [BANKS];
    logic             row_known [BANKS];
    logic [ROW_W-1:0] row_idx   [BANKS];

    // ---- Obligations 2 to 4: per-bank elapsed time. Separate
    //      counters rather than one, because the three thresholds are
    //      measured from DIFFERENT events -- 13.3's scope taxonomy.
    logic [CNT_W-1:0] since_act [BANKS];
    logic [CNT_W-1:0] since_pre [BANKS];

    // ---- Obligation 7: the refresh accrual, held elsewhere. 15.3
    //      owns the ledger with its postponement and pull-in credit
    //      rule; this block CONSUMES `refresh_due` and does not
    //      reimplement the accounting -- 17.3 §7's one-writer rule.
    assign refresh_req = refresh_due;

    // ---- Timekeeping. Saturating, so a long idle period stays legal
    //      rather than wrapping back into illegality.
    always_ff @(posedge clk) begin
      if (!rst_n) begin
        for (int b = 0; b < BANKS; b++) begin
          row_open[b]  <= 1'b0;
          row_known[b] <= 1'b0;
          row_idx[b]   <= '0;
          since_act[b] <= '0;
          since_pre[b] <= '0;
        end
      end else begin
        for (int b = 0; b < BANKS; b++) begin
          if (since_act[b] != {CNT_W{1'b1}}) since_act[b] <= since_act[b] + 1'b1;
          if (since_pre[b] != {CNT_W{1'b1}}) since_pre[b] <= since_pre[b] + 1'b1;
        end
        if (cmd_act) begin
          row_open[cmd_bank]  <= 1'b1;
          row_known[cmd_bank] <= 1'b1;
          row_idx[cmd_bank]   <= req_row;
          since_act[cmd_bank] <= '0;
        end
        if (cmd_pre) begin
          row_open[cmd_bank]  <= 1'b0;
          row_known[cmd_bank] <= 1'b1;
          since_pre[cmd_bank] <= '0;
        end
      end
    end

    // ---- Classification. Against the REGISTERED state, before any
    //      same-cycle command is applied. 30.2 §10 owns why: a
    //      classifier that folds in this cycle's own command reports
    //      the outcome of a state that did not exist when the
    //      requester asked.
    logic [2:0] outcome_c;
    always_comb begin
      outcome_c = OUT_IDLE;
      if (req_valid) begin
        if      (!row_known[req_bank])           outcome_c = OUT_UNKNOWN;
        else if (!row_open[req_bank])            outcome_c = OUT_MISS;
        else if (row_idx[req_bank] == req_row)   outcome_c = OUT_HIT;
        else                                     outcome_c = OUT_CONFLICT;
      end
    end
    assign access_outcome = outcome_c;

    // ---- Obligation 12: legality as a MAXIMUM over every applicable
    //      rule. 13.3 owns the derivation; 30.3 §3 owns why a single
    //      parameter yields only a lower bound. Each term below is
    //      one candidate in that maximum.
    logic legal_col, legal_act, legal_pre;
    assign legal_col = row_open[req_bank] && (since_act[req_bank] >= TRCD[CNT_W-1:0]);
    assign legal_act = !row_open[req_bank] && (since_pre[req_bank] >= TRP[CNT_W-1:0]);
    assign legal_pre = row_open[req_bank]  && (since_act[req_bank] >= TRAS[CNT_W-1:0]);

    // ---- Command emission. One command per cycle: there is one
    //      command bus, and 17.1 owns the commit point as the single
    //      instant architectural state may change.
    always_comb begin
      cmd_pre = 1'b0;
      cmd_act = 1'b0;
      cmd_col = 1'b0;
      cmd_bank = req_bank;
      if (req_valid && refresh_req == 1'b0) begin
        unique case (outcome_c)
          OUT_HIT      : cmd_col = legal_col;
          OUT_MISS     : cmd_act = legal_act;
          OUT_CONFLICT : cmd_pre = legal_pre;
          // OUT_UNKNOWN emits NOTHING. 7.4: an unknown bank state is
          // a finding, and treating it as idle issues an ACT to a
          // bank that may already have a row open -- an ILLEGAL
          // command rather than a slow one.
          default      : ;
        endcase
      end
    end

    // Accept only when the access is COMPLETE, i.e. a column command
    // issued. A miss or conflict re-presents: one request is many
    // commands (17.1 §3), which is §6's variability at its source.
    assign req_ready = cmd_col;

    // §5's count, reported rather than claimed.
    assign obligation_count = 8'd13;

  // ===================================================================
  // THE NON-DESTRUCTIVE CONTRACT. Note what is absent, and that its
  // absence is STRUCTURAL rather than a simplification (§4's
  // inversion): there is no rebuilt copy needing a home, so there is
  // nothing for a row buffer to hold and no state to keep coherent.
  // ===================================================================
  end else begin : g_static

    // No row state. No elapsed counters. No rolling window. No
    // ledger. The access is architecturally one operation -- 1.3 §4.
    assign refresh_req      = 1'b0;
    assign access_outcome   = req_valid ? OUT_STATIC : OUT_IDLE;
    assign obligation_count = 8'd0;

    assign cmd_pre  = 1'b0;
    assign cmd_act  = 1'b0;
    assign cmd_col  = req_valid;
    assign cmd_bank = req_bank;

    // The whole legality question, for this contract. Not "a maximum
    // over every applicable rule" -- there are no rules, because
    // there is no history for a rule to constrain (§4, C2).
    assign req_ready = req_valid;

  end
  endgenerate
endmodule

10. What Vanishes, Counted in Logic

DERIVED from §9's generate boundaries, as state elements rather than as gates, because state is what must be kept coherent with a device the controller cannot observe — and therefore where the bugs are:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   ILLUSTRATIVE BANKS = 16, ROW_W = 16, CNT_W = 6 (from TRAS = 34).

   DYNAMIC configuration, per bank:
       row_open     1
       row_known    1
       row_idx     16
       since_act    6
       since_pre    6
                   --
                   30 bits per bank

       x 16 banks           = 480 bits of state
       + the refresh ledger  (15.3, held outside this block)
       + the rolling activate window (14.8, deliberately omitted
         here -- see the block's LIMITATIONS)

   STATIC configuration:
       0 bits.

   DERIVED: 480 bits against 0, for a frontend that presents the
   IDENTICAL requester interface. The comparison is not "how fast"
   -- it is "how much state must agree with something you cannot
   see", and that is the quantity §5 argues is the real cost.

And the qualification that keeps this honest. 480 bits is not a large amount of silicon. The cost is not the flip-flops — it is that every one of them is a place where the controller's belief can diverge from the device's actual condition, which is 30.2's stale-state hazard, 30.3's convention hazard and 30.7 §9's unread-input hazard all in one structure. The static contract has no such surface because it has no belief to hold.

11. RTL Review — A Completion Model

The intended contract:

  1. Assert done exactly once for each accepted request.
  2. done must not be asserted earlier than the technology's minimum completion time, nor later than its maximum.
  3. For a non-destructive contract, completion is a constant, so the minimum and maximum are equal and FIXED_LAT fully describes it.
  4. For a destructive contract, completion depends on the access outcome — §6's three paths — so the model must consume the outcome rather than a constant.
  5. done must never be asserted for a request that was not accepted.
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ---------------------------------------------------------------------
// access_completion_model -- INTENTIONALLY DEFECTIVE, for review (§11).
//
// CLASSIFICATION: synthesisable, ILLUSTRATIVE, and CONTAINS A BUG.
//
// WHAT IT IS MEANT TO DO: the five-clause contract above -- tell the
// requester when its data is usable, under EITHER access contract.
//
// WHY IT EXISTS HERE: §1 claims admissibility must be checked before
// affordability, and that a block which assumes a constant completion
// time is INADMISSIBLE for a destructive-read technology at any
// capacity. This block is that inadmissibility, in hardware, and it
// is the signature bug class of a parameterised comparison: code
// correct under one parameterisation, reused under the other without
// re-deriving the contract.
//
// HOW TO RUN IT: elaborate with DESTRUCTIVE_READ = 1 and present a
// row-conflict access.
// EXPECTED RESULT under clause 4: `done` is asserted only after the
// conflict path's completion time.
// EXPECTED TRACE: done must follow the precharge, the activate and
// the column command -- not a fixed offset from acceptance.
//
// SYNTHESIS: one shift register of depth FIXED_LAT + 1.
// LIMITATIONS: models ONE outstanding access. Association of several
// returns to several requests is 10.5 §9's, and the single-
// outstanding assumption is STATED rather than hidden -- it is not
// what the bug is.
// ---------------------------------------------------------------------
module access_completion_model #(
  parameter bit DESTRUCTIVE_READ = 1'b1,
  parameter int FIXED_LAT        = 3,    // ILLUSTRATIVE
  // ILLUSTRATIVE, and present on the interface for clause 4's use.
  parameter int TRCD             = 14,
  parameter int TRP              = 14
)(
  input  logic       clk,
  input  logic       rst_n,

  input  logic       req_accept,     // a request was accepted this cycle
  input  logic [2:0] access_outcome, // from §9's frontend
  output logic       done
);
  localparam int DEPTH = FIXED_LAT + 1;

  initial begin
    if (FIXED_LAT < 1) $fatal(1, "access_completion_model: FIXED_LAT >= 1");
  end

  // pipe[i] == 1 means "a completion is due i cycles from now".
  logic [DEPTH-1:0] pipe;

  always_ff @(posedge clk) begin
    if (!rst_n) begin
      pipe <= '0;
    end else begin
      logic [DEPTH-1:0] nxt;
      nxt = pipe >> 1;
      if (req_accept)
        nxt[FIXED_LAT] = 1'b1;      // <-- THE DEFECT
      pipe <= nxt;
    end
  end

  // Clause 5 holds: nothing enters the pipe without an acceptance.
  assign done = pipe[0];
endmodule

Before reading on: which clause, and which of §9's two configurations was this written against?

12. The Defect — Sound Under One Contract, Unsound Under the Other

The violated clause is 4, and the defect is that access_outcome is declared on the port list and never read. Completion is modelled as a constant offset FIXED_LAT from acceptance, under both configurations.

That is exactly correct for the non-destructive contract. Clause 3 says so: minimum equals maximum, and FIXED_LAT fully describes it. So under DESTRUCTIVE_READ = 0 this block is not merely adequate — it is right, and no test will ever fail.

Under DESTRUCTIVE_READ = 1 it reports completion before the data exists.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   ILLUSTRATIVE FIXED_LAT = 3, TRP = 14, TRCD = 14.
   A row CONFLICT access, using §6's path costs.

   cycle   event                                    done
   -----   ------------------------------------     ----
     0     req accepted; outcome = OUT_CONFLICT      0
     1     frontend emits PRE (legal_pre held)       0
     3     this block: pipe[0] -> DONE               1   <-- WRONG
    15     TRP elapsed; frontend emits ACT           0
    29     TRCD elapsed; frontend emits COL          0
    ~34    data actually usable                      0

   the requester was told its data was ready on cycle 3 and the
   column command had not been issued by cycle 29. It reads
   whatever the return path happened to hold.

   and note the DIRECTION: the model is EARLY. An early completion
   is a data-correctness failure. A late one would merely be slow.

Now the review question that matters: why did this survive?

Because it was developed against the static configuration and reused under the dynamic one as “the same module with a different parameter”. The parameter list even invites it — DESTRUCTIVE_READ is present, so the module looks contract-aware, and access_outcome is on the interface, so it looks as though the outcome is consumed. Nothing in the code's shape reveals that the causal parameter changes nothing about the behaviour.

And the reuse was defensible at the time. §9's frontend really is one module serving both contracts, so the precedent for parameterised reuse was established by a block where it was correct. This is the hazard a comparison module has to name, and it is worth stating as a rule:

A parameter that selects a CONTRACT is not the same kind of parameter as one that selects a SIZE. Re-deriving the obligation is mandatory when a contract parameter changes; re-deriving it is unnecessary when a width changes. A parameter list that mixes the two kinds without marking them invites exactly this bug.

The correction, and the deletion is as important as the addition:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  // CORRECTED. Clause 4: under the destructive contract, completion
  // DEPENDS ON THE OUTCOME, so the outcome must be read. The
  // FIXED_LAT path survives only where clause 3 licenses it.
  //
  // Note that `pipe` is gone for the dynamic case rather than being
  // reloaded with a bigger constant: a longer fixed offset would be
  // a WORSE bug, because it would be right on average and wrong per
  // access, and §6's whole point is that the per-access spread is
  // the quantity that decides admissibility.
  generate
  if (DESTRUCTIVE_READ) begin : g_dyn_done
    // Completion is the COLUMN COMMAND's data return, and the column
    // command's timing is the frontend's business. So this block
    // stops predicting and starts OBSERVING: `col_commit` is an
    // input, and done follows it by the device's read latency.
    // 10.2 owns that latency; 30.4 §3 Q4 owns why the observed
    // offset legitimately exceeds it.
    logic [RD_LAT:0] rpipe;
    always_ff @(posedge clk) begin
      if (!rst_n) rpipe <= '0;
      else begin
        logic [RD_LAT:0] nxt;
        nxt = rpipe >> 1;
        if (col_commit) nxt[RD_LAT] = 1'b1;
        rpipe <= nxt;
      end
    end
    assign done = rpipe[0];
  end else begin : g_stat_done
    // Clause 3. Unchanged, and still correct.
    logic [FIXED_LAT:0] pipe;
    always_ff @(posedge clk) begin
      if (!rst_n) pipe <= '0;
      else begin
        logic [FIXED_LAT:0] nxt;
        nxt = pipe >> 1;
        if (req_accept) nxt[FIXED_LAT] = 1'b1;
        pipe <= nxt;
      end
    end
    assign done = pipe[0];
  end
  endgenerate

And the structural finding, which outlives both versions: the corrected dynamic path stops predicting completion and starts observing it. That is the real lesson of the defect — under a history-dependent contract, a completion time is not a number the requester's side can compute, so a model that computes one has assumed the contract away. The static contract permits prediction; the dynamic contract permits only observation.

One interface note, because the correction is not free. The dynamic branch consumes two things the defective block did not have: col_commit, an observation of the frontend's column command, and RD_LAT, the device's configured read latency. Both must be added to the module's interface, and saying so is part of the fix — a correction that silently references signals the module does not have is not a correction. The static branch's interface is unchanged, which is itself the point: the contract parameter changed the interface, and a contract parameter that does not change the interface should be suspected.

13. RTL — Auditing the Parameterisation Itself

§9's elaboration guard enforces §4's causal chain. That is necessary and not sufficient, because a guard can only check the parameters it is given — it cannot check that the two configurations were both exercised, which §12 shows is where the bug lived.

So the third block is verification-only and it audits the comparison rather than the memory.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ---------------------------------------------------------------------
// contract_consistency_audit -- verification-only, CORRECT as written.
//
// CLASSIFICATION: verification/telemetry. Drives nothing. ILLUSTRATIVE
// parameter values.
//
// WHAT IT DOES: confirms at runtime that the CONTRACT the parameters
// claim is the contract the design actually presents, and records
// which contracts a regression exercised.
//
// WHY IT EXISTS HERE: §12's defect was not a parameter error -- the
// parameters were right. It was a BEHAVIOUR that did not follow its
// parameters, plus a regression that never ran the other
// configuration. An elaboration guard catches the first class and is
// blind to the second. This block catches both, and its
// `contracts_seen` output is the artifact a sign-off review should
// ask for -- 30.9 §11's argument that a green result without a
// coverage artifact is not evidence.
//
// HOW TO RUN IT: bind one instance per frontend instance, in both
// configurations, and read `contracts_seen` at end of test.
// EXPECTED RESULT: contracts_seen == 2'b11 after a regression that
// ran both. Anything less is a STIMULUS finding, not a design one.
//
// SYNTHESIS: none -- verification only.
// LIMITATIONS: audits the CONTRACT, not the data. A scoreboard with
// an independent memory model is 27.4's, and this block deliberately
// does not duplicate it -- 27.3's independence requirement means an
// auditor that recomputed the data would share the design's state.
// ---------------------------------------------------------------------
module contract_consistency_audit #(
  parameter bit DESTRUCTIVE_READ = 1'b1,
  parameter bit NEEDS_REFRESH    = DESTRUCTIVE_READ,
  parameter int FIXED_LAT        = 3,
  parameter int OBS_W            = 16
)(
  input  logic       clk,
  input  logic       rst_n,

  input  logic       req_valid,
  input  logic       req_ready,
  input  logic       done,
  input  logic [2:0] access_outcome,
  input  logic [7:0] obligation_count,
  input  logic       refresh_req,

  // Bit 0 set once a static contract was observed, bit 1 a dynamic
  // one. A regression reporting 2'b01 has proved nothing whatever
  // about the dynamic configuration -- and vice versa.
  output logic [1:0]        contracts_seen,
  // Observed spread between the fastest and slowest completion, in
  // cycles. For a static contract this must stay at zero: it is the
  // RUNTIME evidence for clause 3, and 30.8 §11's rule that a
  // derived statistic needs its range asserted applies to it.
  output logic [OBS_W-1:0]  completion_spread,
  output logic              audit_fail
);
  localparam logic [2:0] OUT_STATIC   = 3'd1;
  localparam logic [2:0] OUT_HIT      = 3'd2;
  localparam logic [2:0] OUT_MISS     = 3'd3;
  localparam logic [2:0] OUT_CONFLICT = 3'd4;
  localparam logic [2:0] OUT_UNKNOWN  = 3'd5;

  // COUNT, not INDEX: these hold elapsed cycle counts up to their
  // saturation value and are COMPARED against each other, so both
  // must be wide enough to represent the worst completion the test
  // can produce. Sized too narrowly, min and max both saturate and
  // the spread reads ZERO -- the instrument reporting the static
  // contract's signature while measuring the dynamic one.
  logic [OBS_W-1:0] elapsed, obs_min, obs_max;
  logic             in_flight, seen_any;

  always_ff @(posedge clk) begin
    if (!rst_n) begin
      contracts_seen    <= 2'b00;
      completion_spread <= '0;
      audit_fail        <= 1'b0;
      elapsed           <= '0;
      obs_min           <= '1;     // AND-accumulator discipline: a
      obs_max           <= '0;     // minimum must start at all-ones,
      in_flight         <= 1'b0;   // or the first sample never wins.
      seen_any          <= 1'b0;
    end else begin
      // ---- Which contract is this instance presenting? -------------
      if (access_outcome == OUT_STATIC) contracts_seen[0] <= 1'b1;
      if (access_outcome inside {OUT_HIT, OUT_MISS, OUT_CONFLICT, OUT_UNKNOWN})
        contracts_seen[1] <= 1'b1;

      // ---- Completion-time observation. -----------------------------
      if (req_valid && req_ready) begin
        elapsed   <= '0;
        in_flight <= 1'b1;
      end else if (in_flight && elapsed != {OBS_W{1'b1}}) begin
        elapsed   <= elapsed + 1'b1;
      end

      if (in_flight && done) begin
        in_flight <= 1'b0;
        seen_any  <= 1'b1;
        if (elapsed < obs_min) obs_min <= elapsed;
        if (elapsed > obs_max) obs_max <= elapsed;
        completion_spread <= (elapsed > obs_max ? elapsed : obs_max)
                           - (elapsed < obs_min ? elapsed : obs_min);
      end

      // ---- The audit itself. ----------------------------------------
      // A1: the obligation count must agree with the contract. §9
      // reports 13 or 0, and any other value means the frontend and
      // the audit disagree about which contract elaborated.
      if (obligation_count != (DESTRUCTIVE_READ ? 8'd13 : 8'd0))
        audit_fail <= 1'b1;

      // A2: a static contract must never request maintenance. §4's
      // causal chain says charge that does not leak on a read has no
      // reason to leak while idle.
      if (!DESTRUCTIVE_READ && refresh_req)
        audit_fail <= 1'b1;

      // A3: a static contract must never report a history-dependent
      // outcome, because there is no history (C2).
      if (!DESTRUCTIVE_READ &&
          access_outcome inside {OUT_HIT, OUT_MISS, OUT_CONFLICT})
        audit_fail <= 1'b1;

      // A4: clause 3, checked at RUNTIME rather than assumed. A
      // static contract's completions must all be the same length.
      // This is the check that would have caught §12's defect from
      // the OTHER direction -- not by finding the dynamic case
      // early, but by proving the static case really is constant and
      // therefore that a constant model is licensed ONLY there.
      if (!DESTRUCTIVE_READ && seen_any && completion_spread != '0)
        audit_fail <= 1'b1;
    end
  end
endmodule

The output worth asking for at sign-off is contracts_seen. CURRICULUM-DERIVED from 30.9 §11, which owns the argument that a green regression without a coverage artifact is not evidence: a regression reporting 2'b01 has proved nothing about the dynamic configuration, and §12's defect lives entirely in the bit that was never set.

14. SVA Review — A Property Whose Soundness Depends on a Parameter

The property that was written for §11's block, and it is a good property:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  // Offered as "proves the completion model meets its contract".
  // Correct, non-vacuous, reset-guarded, conventional.
  property p_done_after_fixed_lat;
    @(posedge clk) disable iff (!rst_n)
      req_accept |-> ##FIXED_LAT done;
  endproperty
  assert property (p_done_after_fixed_lat)
    else $error("completion did not arrive at the fixed latency");

Q. Under DESTRUCTIVE_READ = 0 this property is correct and non-vacuous. What does it do under DESTRUCTIVE_READ = 1?

It does not merely fail to catch the defect. It requires it.

Under the dynamic configuration the property states that completion arrives FIXED_LAT cycles after acceptance — which is precisely the defective behaviour. A corrected design that waits for the column command fails this assertion. So the property is not weak evidence; it is an active endorsement of the bug, and it will be cited to reject the fix.

This is a tenth variety, and it is specific to parameterised comparison. Chapter 30.9 §6 collects nine reasons a green assertion proves nothing. None of them covers this, because in all nine the property is inert — too narrow, unnamed, inexpressible, consistency-only, unasserted, vacuous, dependent, conservatism-blind, reset-shadowed. Here the property is active and wrong:

Variety 10 — PARAMETER-CONDITIONAL SOUNDNESS. A property written against one configuration of a parameterised design can be sound under that configuration and, under another, encode the defect as the requirement. Such a property does not fail to detect the bug; it certifies it, and it will be produced as evidence against the correction.

Held against the nine, the distinction is the direction of the error:

VarietyWhere it came fromWhat the property does wrong
1 too narrow in time30.4 §9checks less than the contract
2 key signal unnamed30.5 §11checks something adjacent
3 obligation inexpressible30.6 §11cannot express the contract at all
4 shares a wrong constant30.7 §10confirms agreement, not truth
5 range never asserted30.8 §11omits a check that was free
6 vacuous27.2was never evaluated
7 not independent27.3shares the design's state
8 conservatism-blind30.3 §9cannot see too slow
9 reset-shadowed30.10 §12guarded against its own subject
10 parameter-conditionalthis chapterasserts the defect as the requirement

Varieties 1 to 9 leave a bug undetected. Variety 10 defends it. That is why it is worth a name.

What actually covers the contract across both configurations:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  // Clause 2, written so that it is sound under BOTH configurations.
  // The window is derived from the CONTRACT, not from a constant, so
  // the parameter changes the bound rather than the property's
  // meaning. This is the general repair for variety 10: make the
  // parameter an input to the BOUND, never to the CLAIM.
  property p_done_within_contract_window;
    @(posedge clk) disable iff (!rst_n)
      req_accept |-> ##[MIN_COMPLETION:MAX_COMPLETION] done;
  endproperty
  assert property (p_done_within_contract_window)
    else $error("completion outside the contract's stated window");

  // Clause 3, and it is the property that LICENSES the constant
  // model where the constant model is legitimate. An invariant
  // rather than an implication: under a static contract there is no
  // cycle on which a variable completion is acceptable.
  property p_static_completion_is_constant;
    @(posedge clk) disable iff (!rst_n)
      (!DESTRUCTIVE_READ) -> (completion_spread == '0);
  endproperty
  assert property (p_static_completion_is_constant)
    else $error("a static contract produced a variable completion");

  // Clause 4, which is the one §11 violates. It names
  // `access_outcome` -- the signal the defective block declares and
  // never reads -- and so it cannot pass while that signal is
  // ignored. Variety 2's repair, applied here.
  property p_conflict_waits_for_column;
    @(posedge clk) disable iff (!rst_n)
      (req_accept && access_outcome == OUT_CONFLICT) |->
        (!done throughout (##[1:$] col_commit));
  endproperty
  assert property (p_conflict_waits_for_column)
    else $error("completion reported before the column command issued");

  // Clause 5.
  property p_no_unsolicited_done;
    @(posedge clk) disable iff (!rst_n)
      done |-> $past(in_flight, 1);
  endproperty
  assert property (p_no_unsolicited_done)
    else $error("done asserted with no access in flight");

  // ---- The covers. Without these, every assertion above is a
  //      statement about whichever configuration happened to run.
  // The CONFIGURATION cover. This is variety 10's specific
  // instrument: a regression must be shown to have entered BOTH
  // arms, and §13's contracts_seen is the persistent form of it.
  cover property (@(posedge clk) disable iff (!rst_n)
                  access_outcome == OUT_STATIC);
  cover property (@(posedge clk) disable iff (!rst_n)
                  access_outcome == OUT_CONFLICT);
  cover property (@(posedge clk) disable iff (!rst_n)
                  access_outcome == OUT_MISS);
  // The three-valued case, which is a FINDING rather than a path
  // (7.4). A non-zero count here means the frontend was asked about
  // a bank whose state it had never established.
  cover property (@(posedge clk) disable iff (!rst_n)
                  access_outcome == OUT_UNKNOWN);
  // The antecedent of p_conflict_waits_for_column, published
  // separately: a zero here means no conflict was ever accepted, and
  // that is a different fact from the property holding.
  cover property (@(posedge clk) disable iff (!rst_n)
                  req_accept && access_outcome == OUT_CONFLICT);
  // And the boundary: a hit whose elapsed time equals the minimum.
  // 30.3 §10 -- a rule that is respected but never exercised at its
  // boundary has not been tested.
  cover property (@(posedge clk) disable iff (!rst_n)
                  done && elapsed == MIN_COMPLETION);

The configuration covers are the section's deliverable. A parameterised comparison needs a coverage item per configuration, and it needs it more than an unparameterised design needs anything — because §12's entire failure was a configuration nobody ran while holding a property that would have blocked the fix if they had.

Follow-up an interviewer should ask: could the two configurations share one assertion set at all? Yes, and p_done_within_contract_window is how — the parameter feeds the bound and not the claim. The general repair for variety 10 is that test: does the parameter change what is being asserted, or only the numbers inside it? If it changes the claim, two assertion sets are required and each must be shown to have run.

And the frontend of §9 needs its own set, which the review above did not supply. That omission is worth naming rather than quietly fixing: §14 reviewed the completion model's properties and left the block that actually emits commands unasserted — and an unasserted legality frontend is the single most expensive gap available, because 17.4 §3 owns the result that a broken legality filter issues illegal commands with a perfectly sensible-looking grant.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  // ---- The frontend's contract (§9), asserted. Every property here
  //      names a signal the frontend drives, so none of them can
  //      pass while that signal is ignored -- variety 2's repair.

  // One command bus, so at most one command may commit per cycle.
  // 17.1 owns the commit point as the single instant architectural
  // state may change. An INVARIANT: there is no cycle on which two
  // commands are acceptable, so there is no antecedent.
  property p_one_command_per_cycle;
    @(posedge clk) disable iff (!rst_n)
      $onehot0({cmd_pre, cmd_act, cmd_col});
  endproperty
  assert property (p_one_command_per_cycle)
    else $error("more than one command committed in a cycle");

  // Legality: no column command before the row-to-column separation
  // has elapsed. 30.3 §5's convention is STATED here -- the column
  // command may issue ON act_cycle + TRCD -- so the comparison is
  // >=, and the property encodes the stated reading rather than
  // silently choosing one of the three.
  property p_no_column_before_trcd;
    @(posedge clk) disable iff (!rst_n)
      (cmd_col && DESTRUCTIVE_READ) |->
        (row_open[cmd_bank] && since_act[cmd_bank] >= TRCD);
  endproperty
  assert property (p_no_column_before_trcd)
    else $error("column command issued before TRCD elapsed");

  // The three-valued discipline of 7.4, as a safety property: an
  // unknown bank state must produce NO command. Treating unknown as
  // idle issues an ACT to a bank that may already have a row open,
  // which is an illegal command rather than a slow one.
  property p_unknown_emits_nothing;
    @(posedge clk) disable iff (!rst_n)
      (access_outcome == OUT_UNKNOWN) |->
        (!cmd_pre && !cmd_act && !cmd_col);
  endproperty
  assert property (p_unknown_emits_nothing)
    else $error("a command was emitted for an unknown bank state");

  // Never activate a bank that already has a row open. This is the
  // property that the OUT_UNKNOWN case exists to protect, and it is
  // stated separately because the two failures have different causes.
  property p_no_act_to_open_bank;
    @(posedge clk) disable iff (!rst_n)
      (cmd_act && DESTRUCTIVE_READ) |-> !row_open[cmd_bank];
  endproperty
  assert property (p_no_act_to_open_bank)
    else $error("activate issued to a bank with a row already open");

  // The CONTRACT invariant, and the one property that is sound under
  // BOTH configurations without reinterpretation -- because the
  // parameter appears in the ANTECEDENT as a condition on the
  // configuration, never inside the claim. §14's test, passed.
  property p_static_emits_no_row_commands;
    @(posedge clk) disable iff (!rst_n)
      (!DESTRUCTIVE_READ) -> (!cmd_pre && !cmd_act);
  endproperty
  assert property (p_static_emits_no_row_commands)
    else $error("a static contract emitted a row-state command");

  // §13's audit, asserted rather than merely reported: the frontend's
  // self-declared obligation count must match the contract its
  // parameters claim. An INVARIANT, and it is the cheapest possible
  // check that the configuration that elaborated is the one intended.
  property p_obligation_count_matches;
    @(posedge clk) disable iff (!rst_n)
      obligation_count == (DESTRUCTIVE_READ ? 8'd13 : 8'd0);
  endproperty
  assert property (p_obligation_count_matches)
    else $error("obligation count disagrees with the contract parameters");

  // ---- Two more covers, both antecedents published separately.
  // The legality boundary for the OTHER two rules, so that "the rule
  // was respected" is distinguishable from "the rule was exercised".
  cover property (@(posedge clk) disable iff (!rst_n)
                  cmd_act && since_pre[cmd_bank] == TRP);
  cover property (@(posedge clk) disable iff (!rst_n)
                  cmd_pre && since_act[cmd_bank] == TRAS);

p_static_emits_no_row_commands is the model answer to §14's test. The parameter sits in the antecedent, as a condition on which configuration is being discussed, and the claim — no row-state commands — is the same sentence in both arms. Contrast p_done_after_fixed_lat, where the parameter sat inside the claim and the claim therefore changed meaning. That is the distinction to apply before reusing any property across a contract parameter.

15. Where the Boundary Sits Inside One Chip

§3 observed that a memory controller contains memory. Develop it, because it is the form this decision actually takes in practice.

Nobody at work is asked SRAM or DRAM for main memory1.7 settled that and it stays settled. What engineers are asked, repeatedly, is where a particular buffer should live, and §7's expression answers it differently for each one because K is nearly zero once the chip already has a controller.

BlockAdmissible under the dynamic contract?Why
The scheduler's own command queuenoit must be read every cycle to compute legality; a variable-latency read cannot serve a combinational decision
A trained-value registernoread on the critical launch path, and 30.7 §9 shows an untrained value must be refused, not awaited
A first-error snapshotno30.10 §11 — it must survive a reset; a dynamic memory needs a controller that a reset disturbs
A reorder buffer for returnsborderlineadmissible if the requester tolerates the spread; the deciding quantity is §6's ratio
A packet or descriptor bufferoften yesthroughput-shaped, latency-tolerant, and large — which is 31.4's whole subject
A frame or tile bufferyeslarge, streamed, and the consumer is deliberately pipelined deep enough to hide the spread

Rows one to three are the useful ones, and they are all admissibility failures rather than cost decisions. The scheduler cannot be served from the memory it schedules, and stating that is a better answer than any capacity argument: the dependency is circular, so the question is closed before cost is considered.

And rows five and six are where the crossover has genuinely moved. As K goes to zero for a chip that already has a controller, blocks that were once obviously on-die become candidates for main memory — which is why the boundary is a design-review question per project rather than a fact to memorise. CURRICULUM-DERIVED from 1.7 §6, which owns the observation that the tier boundaries are moving; this section is that observation applied one level down, to blocks rather than tiers.

16. What Would You Measure?

Q. You must decide whether a new on-chip buffer can be served from DRAM instead. What do you measure, in what order?

MeasurementWhat it settlesCostOwner
The consumer's deadline, if it has oneadmissibility — §1 stage one, and it is a specification question, not a measurementhours§1
Outstanding-transaction capacity of the requesterwhether the spread can be hidden at allhours29.3
The access pattern's row-conflict rate§6's spread for this traffic rather than worst casedays23.3
Whether the block is on a combinational or cycle-critical patha hard no, if it is — §15 rows one to threehours§15
The marginal controller cost Kwhether a controller already exists to reusehours§7
Required capacity, against §7's formstage two, and only after stage one passedhours1.6
Idle power at the required capacity for the static optionthe constraint that usually binds above N*days1.3 §4
contracts_seen from §13, per configurationwhether anything was ever verified in the configuration you are proposingfree§13

Rows one and four cost hours and can close the question, which is why they come before the capacity work that everyone starts with. An investigation that begins with capacity has begun at stage two.

Row seven is the one that reverses the naive conclusion above N*. Chapter 1.3 §4 is explicit that a static array's consumption is proportional to how much storage exists rather than how much of it is in use. So a large, mostly-idle static buffer is the worst case for that mechanism, and a comparison run only on area will select it and then be redesigned when the power budget closes.

And row eight is free. If the configuration you are about to ship has never been exercised, that is a finding available before any measurement — §13's whole purpose.

17. Common Wrong Answers

“SRAM is faster DRAM.” §4. It is a different kind of cell: the read is non-destructive, so twelve of §5's thirteen obligations do not exist. The speed is a consequence, not the difference.

“Static means non-volatile.” §3. Static is a claim about maintenance, not persistence — 1.3 §1. Both lose their contents without power.

“Pick by capacity.” §1. That is stage two. A block that cannot tolerate a variable completion time is inadmissible at any capacity, and running stage two first turns a feasibility question into a correctness bug.

“DRAM is slower, so budget a bigger latency.” §6. The problem is not the mean, it is the spread — three outcomes at an illustrative ratio of 2.40, and 23.1 owns the component with no upper bound at all.

“Compare the cell areas.” §7. The comparison must carry P_d and, above all, K — the controller. K has a floor and does not scale down, so the dynamic contract gets relatively more expensive as the instance shrinks.

“A smaller DRAM is a cheaper DRAM.” §7. Below the crossover it is strictly worse on every axis at once — worse latency, worse variability, more area, same volatility.

“SRAM wins on power because DRAM needs refresh.” §7, and 1.3 §4 owns the correction: a static array consumes in proportion to how much storage exists, not how much is in use. Above the crossover, idle power is usually what selects the dynamic option.

“Give the SRAM a row buffer to make the comparison fair.” §4. There is nothing to hold — the cell kept its value. What you have added is a cache, which answers a different question.

“SRAM is simpler.” §5. Fewer obligations, not zero, and of a different kind — 1.3 §6 owns the single-port exclusivity cost and multi-porting is its own hard problem.

“DDR's bank parallelism escapes the one-row-at-a-time limit.” §3. It is a response to the same shared-periphery exclusivity 1.3 §3 derives, not an escape from it.

“The frontend is the same module, so a parameter change is safe.” §12. A parameter that selects a contract is not the same kind of parameter as one that selects a size, and the obligation must be re-derived when the first kind changes.

access_outcome is on the port list, so the model is outcome-aware.” §12. It is declared and never read — the static tell of 30.7 §9 and 30.8 §10, and lint finds it before simulation starts.

“Use a longer fixed latency to be safe.” §12. Right on average and wrong per access, which is the one property §6 says decides admissibility. And the correction is not a bigger constant — it is to stop predicting and start observing.

“The completion assertion passes, so the model is right.” §14. Under the other configuration that same assertion requires the defect and will be produced as evidence against the fix. Variety 10.

“One assertion set serves both configurations.” §14. Only if the parameter feeds the bound rather than the claim. Apply that test before reusing a property across a contract parameter.

“The regression is green.” §13, §14. In which configuration? contracts_seen of 2'b01 is a green result that says nothing about the arm you are shipping.

“This decision was made once, for main memory.” §15. It is made per block, repeatedly, and the crossover moves — most sharply because K is nearly zero on a chip that already has a controller.

“We could serve the command queue from DRAM to save area.” §15. The scheduler cannot be served from the memory it schedules: the dependency is circular, so the question closes at stage one without any cost analysis.

18. Self-Check

  1. State the two stages of the decision in order, and explain why running them in the other order produces a correctness bug rather than a cost overrun.

  2. Write the four questions of the access contract, and answer all four for both technologies from the cell property alone.

  3. Name the seven axes. Then say which one is causal in this chapter's comparison and which three it explains.

  4. From §5's table, name five obligations that exist only under the destructive contract, and for each the chapter that owns it.

  5. Using §6's illustrative path costs, compute the hit, miss and conflict completion times and the spread ratio. Then state two reasons the real spread is worse than that ratio.

  6. Write §7's crossover expression from memory. Explain why the numerator is dominated by K for small systems, and what happens to the crossover on a chip that already has a DDR controller.

  7. In §9, list everything inside if (DESTRUCTIVE_READ) and say, for each item, which of §5's obligations it implements.

  8. Find the defect in §11 without reading §12. Then answer the harder question: which configuration was it developed against, and why did the reuse look defensible?

  9. Explain why p_done_after_fixed_lat is sound under one configuration and certifies the defect under the other. State variety 10 in one sentence, and give the test that distinguishes a property that can be shared across configurations from one that cannot.

  10. For three of §15's blocks, say whether the dynamic contract is admissible and why — and identify which of your three answers required no cost analysis at all.

19. Where This Goes

The choice between these two technologies is not a point on an axis. It is a choice between access contracts, decided in two stages with admissibility first; the destructive-read property is causal and creates twelve of thirteen controller obligations; latency must be compared as a spread in one stated domain rather than as a mean; the crossover carries a controller term with a floor, so a small dynamic memory is strictly worse on every axis; and the boundary is a per-block decision that moves when a controller already exists.

Three results carry forward. A parameter that selects a contract is a different kind of parameter from one that selects a size, and the obligation must be re-derived when it changes. Under a history-dependent contract a completion time cannot be predicted from the requester's side, only observed. And variety 10 — a property can be sound under one configuration and certify the defect under another, which makes a per-configuration coverage artifact mandatory rather than good practice.

Chapter 31.2 keeps the contract and changes the objective. LPDDR and DDR share axis A1 entirely — the same destructive read, the same thirteen obligations, the same bank and row model, the same timing-legality framework — so none of this chapter's structural argument applies, and a reader expecting another difference in kind will find none. What differs is the quantity being minimised, and the deciding quantity turns out to be neither power nor bandwidth but energy under the actual duty cycle — which is why a comparison run on peak numbers picks the wrong part, and why the controller gains a state machine whose illegal transitions are the interesting part.

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.