Skip to content
VLSI Mentor

DDR · Module 10

The Read Command

Accepting a read returns nothing. It starts a timed return pipeline and creates an obligation the controller must carry — which is why a read transaction exists long before any data does.

Module 7 established what a READ command is: an operation encoded on the command/address interface, qualified for a target, naming a column in a row that is already open. Module 8 established where its operands come from. Module 9 established the state it needs to find.

All three stopped at the same place — the moment the command is accepted — and none of them said what happens next.

This module is what happens next, and it begins with the observation that nothing happens next immediately:

Once a read is legal and its row is open, what does acceptance actually start?

The answer is the module's organising idea. Accepting a read returns nothing. It starts a pipeline whose output arrives later, and it creates an obligation the controller must carry in the meantime. A read transaction exists from the moment of acceptance — with no data in it, and nothing on any wire to show for it.

1. Where This Chapter Starts

The precondition is worth stating exactly, because everything downstream assumes it.

A read command is accepted when the bank it names already holds the row it wants. Chapter 9.3 called that a row hit and gave it three conditions: same bank, same row, and a bank in BANK_OPEN.

If those do not hold, there is no read to discuss yet. A closed bank needs an activate (Chapter 9.4); a bank holding a different row needs a precharge and then an activate (Chapter 9.5). Those are row-state transitions, and this module begins after they are complete.

So the state of the world at cycle zero of this module:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   bank B is OPEN, holding row R
   the requester wants column C of that row
   the classification is REQ_HIT
   nothing is on the data bus
   the data bus is not owned by the DRAM

And the question is what acceptance changes.

2. Four Things a READ Does Not Do

Each of these is assumed by somebody, and each assumption produces a different defect.

It does not activate a row. Chapter 9.1 established that only an activate does, and Chapter 8.2 established that a column command carries no row address — there is nowhere for one to go. A read arriving at a closed bank is not a slow read; it is a command whose precondition is absent.

It does not carry the full address. The bank and column are on the wire; the row is in the bank's state, deposited by the activate. Chapter 8.2 §2 derived why: rows change rarely and columns change constantly, so the interface sends the rare field once and stores it in the device.

It does not return data in the same cycle. This is the chapter's subject and Chapter 10.2's. The command is sampled at a command event; the data appears later, on a different set of wires, accompanied by a different timing reference.

It does not mean the controller owns valid data. Even when data eventually appears, it appears at the PHY boundary as captured beats. The controller receives a digital abstraction of a transfer that has already happened. Chapter 10.3 draws that boundary properly.

3. What Acceptance Creates

If acceptance produces no data, what does it produce? An obligation, held by the controller, with nothing on any wire to represent it.

The obligation has to carry three things across the gap between command and data:

Identity. When beats arrive, something must say which request they belong to. With one outstanding read that is trivial; with more than one it is the whole problem, and Chapter 10.5 builds the association.

Metadata. The bank and column the read named, so that a monitor or a scoreboard can check what came back against what was asked for. This is not needed by the device — the device has already been told. It is needed by everything above.

Expectation. When data should appear, which is Chapter 10.2's, and how much, which is Chapter 10.4's. Without an expectation, "data arrived" and "data arrived correctly" are the same statement, and they are not.

None of this exists in the DRAM. The device does not know there is a transaction; it was given a command and it will produce a burst. The transaction is entirely a controller-side construct, and that is why this module's RTL is controller RTL.

A sequence from the requester through the controller to the DRAM and back. The requester presents a read request naming a bank and a column. The controller checks the row-state classification and, on a row hit, admits the request, creating a pending transaction record that holds an identity and metadata. The controller issues the read command to the DRAM. The DRAM performs column selection and internal data movement, which takes time. Later the DRAM drives the data bus and the PHY captures beats, which the controller associates with the pending record. When the final beat arrives the controller retires the record and reports completion to the requester.Acceptance to completionRequesterControllerDRAMPHY boundaryread bank B, col Cadmitted — recordheldREAD issuedburst, latercaptured beatscomplete
Figure 1 — Acceptance creates an obligation that outlives the command by the whole return latency.

Read the gap between the third and fifth arrows. Nothing crosses the interface there. The DRAM is working, the controller is waiting, and the only evidence the transaction exists is the record the controller is holding.

4. RTL — Admitting a Read and Creating a Transaction

The engineering problem

Accept a read request only when its row state permits, and create exactly one transaction record carrying the identity and metadata needed to recognise the return when it eventually arrives.

Why hardware needs it

This sits at the front of the read path. Without a record, returned beats cannot be attributed to anything, and "data arrived" cannot be distinguished from "the right data arrived for the right request."

Classification

SYNTHESIZABLE EDUCATIONAL CONTROLLER RTL.

What it models

Admission of a read against a supplied row-state classification; creation, retention and retirement of a single pending-read record; and rejection with a distinguishable reason.

What it does NOT model

Classification (Chapter 9.3, consumed as an input). Row state (Chapter 9.1). Command encoding (Chapter 7.1). Latency (Chapter 10.2) — nothing here knows when data is due. Data, beats or the bus (Chapters 10.3 and 10.4). Scheduling, queueing, reordering (Module 17). Timing legality (Modules 13 and 14) — admission means the state permits the command, never that it may be issued this cycle.

Interface and parameter contract

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ─────────────────────────────────────────────────────────────────────────
// read_request_admit
//
// Classification: SYNTHESIZABLE EDUCATIONAL CONTROLLER RTL.
//
// MODELS: admission of a read against a supplied row-state classification,
// and creation/retention/retirement of ONE pending-read transaction record.
//
// ONE OUTSTANDING READ. A second request while one is pending is REJECTED
// with a reason, never queued -- queueing is scheduling, and Module 17 owns
// it. Chapter 10.5 revisits multiple outstanding reads.
//
// DOES NOT MODEL: classification (Chapter 9.3, consumed), row state
// (Chapter 9.1), command encoding (Chapter 7.1), LATENCY (Chapter 10.2 --
// nothing here knows when data is due), data or beats (Chapters 10.3,
// 10.4), scheduling (Module 17), or timing legality (Modules 13, 14):
// admission means the STATE permits the command, never that it may be
// issued this cycle.
//
// MODELS NO PHYSICAL OR ANALOG BEHAVIOUR.
//
// GENERATION: GENERATION-NEUTRAL CONTROLLER MODEL.
// ─────────────────────────────────────────────────────────────────────────
module read_request_admit #(
  parameter int NUM_BANKS = 4,
  parameter int COL_W     = 10,
  // Width of the educational transaction identifier. With one outstanding
  // read the tag is not needed to DISAMBIGUATE -- it is here because it is
  // what makes association checkable, and because Chapter 10.5 needs it.
  parameter int TAG_W     = 3,
  parameter int BA_W      = (NUM_BANKS <= 1) ? 1 : $clog2(NUM_BANKS)
) (
  input  logic                 clk,
  input  logic                 rst_n,

  // ── Request from above. Semantic: a bank and a column, already derived
  //    by Module 8's address map.
  input  logic                 req_valid,
  input  logic [BA_W-1:0]      req_bank,
  input  logic [COL_W-1:0]     req_col,

  // ── Row-state classification from Chapter 9.3's row_request_classifier.
  //    Read, never recomputed: a second classifier could disagree with the
  //    first, which is Chapter 9.2's hazard.
  input  req_class_e           req_class,

  // ── Admission handshake.
  output logic                 req_accept,

  // ── Read-command intent, for Chapter 7.1's encoder. NOT an encoding.
  output logic                 rd_cmd_valid,
  output logic [BA_W-1:0]      rd_cmd_bank,
  output logic [COL_W-1:0]     rd_cmd_col,

  // ── THE TRANSACTION RECORD. This is what the chapter is about: it
  //    exists from acceptance until retirement, and for most of that
  //    interval there is nothing on any wire that corresponds to it.
  output logic                 pending,
  output logic [TAG_W-1:0]     pending_tag,
  output logic [BA_W-1:0]      pending_bank,
  output logic [COL_W-1:0]     pending_col,

  // ── Retirement, driven by Chapter 10.5's tracker when the final beat
  //    of the return has been accounted for.
  input  logic                 retire,

  // ── Rejections, reported separately because they need different
  //    responses: a non-hit needs a row-state transition (Module 9), a
  //    busy rejection needs only patience.
  output logic                 reject_not_hit,
  output logic                 reject_busy
);

  if (NUM_BANKS < 1) begin : g_nb
    initial $fatal(1, "read_request_admit: NUM_BANKS must be >= 1");
  end
  if (COL_W < 1) begin : g_cw
    initial $fatal(1, "read_request_admit: COL_W must be >= 1");
  end
  if (TAG_W < 1) begin : g_tw
    initial $fatal(1, "read_request_admit: TAG_W must be >= 1");
  end

  // ── Index legality. The shape Chapter 9.1 Section 5 explains: a cast to
  //    BA_W truncates for a power-of-two NUM_BANKS, making the comparison
  //    permanently false, so the arm is removed where it cannot fire.
  logic idx_bad;
  if (NUM_BANKS >= (1 << BA_W)) begin : g_idx_full
    assign idx_bad = 1'b0;
  end else begin : g_idx_partial
    assign idx_bad = ({1'b0, req_bank} >= (BA_W+1)'(NUM_BANKS));
  end

  // ── Admission. A read is admitted ONLY on a row hit. Every other
  //    classification is a row-state problem that Module 9 must resolve
  //    first, and admitting one here would issue a column command into a
  //    bank that cannot serve it.
  logic is_hit;
  assign is_hit = (req_class == REQ_HIT) && !idx_bad;

  assign req_accept     = req_valid && is_hit && !pending;
  assign reject_not_hit = req_valid && !is_hit;
  assign reject_busy    = req_valid && is_hit && pending;

  // ── Command intent is emitted in the same cycle as acceptance. It is
  //    combinational on purpose: a registered intent would be an intent
  //    for a row state that may have moved, which is Chapter 9.5's
  //    argument against a registered plan.
  assign rd_cmd_valid = req_accept;
  assign rd_cmd_bank  = req_bank;
  assign rd_cmd_col   = req_col;

  // ── The record.
  logic [TAG_W-1:0] tag_ctr_q;

  always_ff @(posedge clk) begin
    if (!rst_n) begin
      pending      <= 1'b0;
      pending_tag  <= '0;
      pending_bank <= '0;
      pending_col  <= '0;
      // Tags restart at zero after reset. A monitor that survives the
      // reset must therefore not assume tags are unique across it --
      // stated here because a silently reused tag is indistinguishable
      // from a duplicated return.
      tag_ctr_q    <= '0;
    end else begin
      if (req_accept) begin
        pending      <= 1'b1;
        pending_tag  <= tag_ctr_q;
        pending_bank <= req_bank;
        pending_col  <= req_col;
        // Wraps deliberately: a free-running identifier, not a counter of
        // anything. Chapter 10.5 explains when wrapping matters.
        tag_ctr_q    <= tag_ctr_q + 1'b1;
      end else if (retire) begin
        pending <= 1'b0;
        // Metadata is NOT cleared on retirement. It is the last record of
        // what was asked for, and Section 9's debugging uses it. pending
        // already says the record is inactive.
      end
    end
  end

endmodule

State representation and transitions

One pending bit, one tag, and the request's metadata. The transition graph is two edges:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   idle     --req_accept-->  pending(tag, bank, col)
   pending  --retire-->      idle

req_accept and retire cannot both fire usefully in the same cycle, because req_accept requires !pending and retire is only meaningful while pending. The else if ordering makes acceptance win if a consumer drives both, which is a stated simplification — Chapter 10.5 is where back-to-back reads are handled properly.

Combinational behaviour

An index check, one equality against the classification, three mutually exclusive handshake outputs, and the command-intent passthrough.

Sequential behaviour

Nonblocking only. The record is written at acceptance and cleared at retirement.

Reset behaviour

pending low, metadata zeroed, tag counter zeroed. The tag restarting at zero is the interesting part: a monitor spanning a reset cannot assume tags are unique across it, and a reused tag is indistinguishable from a duplicated return. That is called out in the code rather than discovered later.

Cycle-by-cycle trace

CycleInputreq_acceptpending afterpending_tagReject
0idle00
1req_valid, b1, c40, HIT110
2req_valid, b1, c41, HIT010busy
3idle010
4retire000
5req_valid, b2, c10, HIT111
6req_valid, b3, c00, MISS011not_hit

Cycles 2 and 6 are both rejections and they are different problems. Cycle 2's request is fine and merely early — the same request will be admitted once the outstanding read retires. Cycle 6's request cannot be admitted at any time until Module 9 opens its row. Reporting one signal for both would leave a controller unable to tell "wait" from "fix something."

How to simulate, and expected output

Drive the trace above and check all six outputs. Then the cases that matter more:

retire with no pending read must change nothing. A spurious retirement is a real failure mode — Chapter 10.5's phantom return — and this block must not silently absorb it.

req_valid with REQ_CONFLICT and with REQ_BUSY must both give reject_not_hit, not acceptance. Cover all four non-hit classes, not just miss.

Acceptance and retirement asserted together — confirm the documented precedence and that pending ends high with the new tag.

Reset while pending must clear the record and restart tags at zero.

NUM_BANKS = 3 with req_bank = 3 must give reject_not_hit via the index check, never acceptance.

Expected waveform

§5. The shape to look for is pending rising with req_accept and staying high through a stretch in which nothing else happens at all — that stretch is the latency Chapter 10.2 is about.

Synthesis implications

One flag, a TAG_W counter, and BA_W + COL_W metadata bits — at the defaults, about 18 flops. Trivially small, and it is the entire reason a returned beat can be attributed to anything. The cost of transaction tracking is not in the record; it is in how many records a controller chooses to keep, which is Module 17's decision.

Corner cases

NUM_BANKS == 1 gives BA_W == 1 through the guard and selects the partial-index arm, so index 1 is correctly rejected. Non-power-of-two NUM_BANKS is the only configuration where the index check fires. TAG_W == 1 is legal and gives two alternating tags — a good configuration for testing wrap, and one where a monitor's uniqueness assumption breaks quickly by design. COL_W == 1 is legal. Tag wrap is not an error and is not reported: the tag is an identifier, not a count.

Failure modes and debugging clues

pending stuck high means retirement is never arriving — the return path is broken, and §9 separates the reasons. reject_not_hit asserting constantly means Module 9's classification is not what this block expects; check the classifier's inputs before this block's. req_accept never asserting with pending low and hits present means the index check is firing — inspect NUM_BANKS against the actual bank count.

Extension ideas

Replace the single record with a small array indexed by tag, and the block supports several outstanding reads — at which point return association stops being trivial and becomes the subject of Chapter 10.5. Adding a returned-beat count to the record turns it into that chapter's tracker.

Limitations

One outstanding read. One rank. No queueing, no ordering, no arbitration. It knows nothing about when data is due or how much of it there will be — both are later chapters, and the record deliberately has no field for either yet.

5. Acceptance, in Cycles

read_request_admit — the obligation outlives the command

10 cycles
Ten cycles. A read request for bank one column forty is presented with a row-hit classification and is accepted at cycle one, asserting the read command intent for a single cycle and setting the pending flag with tag zero. A second request at cycle two is rejected as busy because a read is already outstanding, and the pending record is unchanged. Nothing happens on any interface for several cycles while the pending flag stays high. A retire pulse at cycle eight clears the pending flag. A request at cycle nine with a miss classification is rejected as not-hit rather than accepted.pending — nothing on any wirepending — nothing on any wireaccepted · command issuedaccepted · command issuedsecond read: busysecond read: busyretiredretirednot a hit: refusednot a hit: refusedCKreq_validreq_class--HITHIT------------MISSreq_acceptrd_cmd_validpendingpending_tag0000000000retirereject_busyreject_not_hitt0t1t2t3t4t5t6t7t8t9
Figure 2 — A transaction that exists for eight cycles and contains no data for seven of them. EDUCATIONAL — NOT TO SCALE, NOT JEDEC TIMING.

rd_cmd_valid is high for exactly one cycle. The command is a single event. pending is high for seven.

That asymmetry is the chapter. For six of those seven cycles there is no command, no data, no bus activity, and nothing observable on any interface — and a read transaction is in progress. The only thing that says so is a flag and a tag inside the controller.

Cycles 2 and 9 are both refusals with different meanings. Cycle 2's request will succeed later, unchanged. Cycle 9's will not succeed until Module 9 opens its row.

And the phase between cycles 2 and 8 is deliberately unlabelled as to length. How long it actually is depends on the generation, the configured operating point and the mode registers, which is Chapter 10.2's entire subject. EDUCATIONAL — NOT TO SCALE, NOT JEDEC TIMING.

6. The Three Layers, Named Once

This module crosses more architectural boundaries than any before it, so the layers are named here and used consistently in all five chapters.

LayerOwnsSpeaks in terms of
A — DRAM command and arraythe open row, column selection, internal data movementbanks, rows, columns, commands
B — PHY and electrical transferbus ownership, the strobe, source-synchronous captureDQ, DQS, beats, edges
C — Controller transactionadmission, pending records, association, completionrequests, tags, transactions

Three things follow, and each rules out a sentence people say.

The controller does not sample a DRAM cell. It receives captured beats from a boundary. Layer C never touches layer A.

The PHY does not know which request it is serving. It moves a transfer. Tags are a layer-C concept and appear nowhere on the interfaceChapter 10.3 makes that concrete.

The DRAM does not know a transaction exists. It received a command and will produce a burst. It has no notion of a requester, a tag, or a completion.

This chapter is entirely layer C, which is why its RTL contains no DQ, no DQS and no data. Chapter 10.3 is where layers B and C meet, and drawing that boundary correctly is most of what that chapter does.

7. Four Assertions Worth Writing

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// P1 -- a read is admitted only on a row hit. This is the precondition
// from Section 1 as a contract: admitting any other class would issue a
// column command into a bank that cannot serve it, and the command would
// be perfectly well-formed.
property p_admit_requires_hit;
  @(posedge clk) disable iff (!rst_n)
    req_accept |-> (req_class == REQ_HIT) && !pending;
endproperty
assert property (p_admit_requires_hit);

// P2 -- acceptance creates exactly one pending record, and nothing else
// creates one. Both directions, because the reverse is what catches a
// record appearing from a spurious retire or from reset glitching.
property p_pending_only_from_accept;
  @(posedge clk) disable iff (!rst_n)
    (!$past(pending) && pending) |-> $past(req_accept);
endproperty
assert property (p_pending_only_from_accept);

// P3 -- the record is immutable while it is pending. If bank, column or
// tag could change under an outstanding read, then the metadata a
// returning beat is checked against is not the metadata that was
// requested -- and every scoreboard comparison downstream is meaningless.
property p_record_is_stable_while_pending;
  @(posedge clk) disable iff (!rst_n)
    (pending && $past(pending) && !$past(req_accept))
      |-> (pending_tag  == $past(pending_tag))
          && (pending_bank == $past(pending_bank))
          && (pending_col  == $past(pending_col));
endproperty
assert property (p_record_is_stable_while_pending);

// P4 -- a retirement with nothing outstanding changes nothing. This is
// the phantom-return guard at its earliest point: a spurious retire that
// silently cleared state here would desynchronise the whole read path
// with no evidence left behind.
property p_retire_without_pending_is_inert;
  @(posedge clk) disable iff (!rst_n)
    (retire && !pending) |=> (pending == $past(pending));
endproperty
assert property (p_retire_without_pending_is_inert);

What these prove. P1 pins the precondition, and it is the property that fails if a controller is "optimised" to issue reads speculatively before a row is confirmed open. P2 makes the record's provenance exact. P3 is the most valuable of the four — it is what makes every downstream comparison meaningful, and its failure mode is silent: mutated metadata produces a scoreboard that compares returned data against a request nobody made. P4 is the phantom-return guard.

What these do not prove. Nothing here says data will ever return, or when, or how much — this block has no notion of any of those, and the properties deliberately cannot express them. Nothing says the classification was right: req_class is an input, so Chapter 9.3's properties are what establish it. Nothing proves timing legality — admission is a statement about state, and Modules 13 and 14 own whether the command may be issued this cycle. And nothing proves anything physical: no property here concerns the array, the bus, a strobe or a voltage, and the block contains no representation of them.

8. DV — A Transaction Begins Before Any Data Exists

The verification consequence of this chapter is a structural one, and it shapes the whole read environment.

A read monitor must create its expected transaction at command acceptance, not when data arrives. If the expectation is built from the return, then the return is being checked against itself, and the monitor cannot detect a return that should never have happened.

Which gives the ordering every read-checking environment needs:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   observe accepted READ     →  create expected transaction
                                (bank, column, open-row context, tag)
   ... latency ...              nothing observable
   observe captured beats    →  associate with the expectation
   final beat                →  check count, check data, retire

Four requirements follow, and each is a real failure if skipped:

Capture the open-row context at acceptance, not later. The row is in bank state, not on the command — Chapter 8.2 — and that state can change before the data returns. A monitor that resolves the row at return time may resolve it against a row that has since been replaced, and will compare the returned data against the wrong location in its reference memory.

Keep the expectation even when no data arrives. The most important read bug is a read that returns nothing, and it is invisible to any check that only fires on returned data. An outstanding expectation that never retires is the evidence, which means the monitor needs a notion of an expectation that has aged.

Model rejections too. A rejected request is not a transaction; if a monitor creates an expectation for every presented request rather than every accepted one, it will report missing data for reads that were never issued. §5's cycles 2 and 9 are exactly this hazard.

Do not assume tags survive reset. §4's tag counter restarts, so a monitor spanning a reset must flush its expectations rather than continue matching against identifiers that have been reused.

9. Debugging — A READ Was Issued and No Data Returned

Symptom. A read command is visible on the command interface with correct operands. No data ever returns. The transaction never completes and the requester eventually stalls.

Candidate mechanisms.

  1. The read was never actually admitted — the command on the interface came from somewhere else, or the request was rejected and the rejection ignored.
  2. The read was admitted against a bank that was not actually open, so the device received a column command it could not serve.
  3. Data returned and was not recognised — the return path, the beat count, or the association failed. Chapters 10.3 and 10.4.
  4. Data returned at a different time than expected and the expectation window had closed. Chapter 10.2.
  5. The bus never changed ownership, so the device drove nothing. Chapter 10.3.
  6. retire is never driven, so the record persists even though the data arrived correctly.

Evidence to collect. req_accept, pending and pending_tag at the cycle the command appeared. reject_not_hit and reject_busy for the whole window. The bank's state from Chapter 9.1's model at acceptance. And whether any data-bus activity occurred at all in the expected window — which is the single most discriminating piece of evidence and the one most often missing from a command-only trace.

Discriminator.

  • Was req_accept ever high for this request? If not, mechanism 1, and reject_not_hit versus reject_busy says which. This is one signal and it eliminates the most.
  • Was the bank in BANK_OPEN at acceptance? If not, mechanism 2 — and P1 would have caught it, so its absence from the assertion set is itself the finding.
  • Did the data bus show any activity in the window? This is the layer discriminator. No activity at all points at mechanisms 2 or 5 — the device never produced anything. Activity that the controller did not consume points at 3, 4 or 6, and the fault is above the bus.
  • Is pending still high with data known to have arrived? Mechanism 6. The read worked and the record was never retired, which is a completion-path bug, not a read bug.
  • Did beats arrive but fewer than expected? Mechanism 3, and Chapter 10.4's beat collector is where that is detected.

Responsible layer. Mechanism 1 is layer C, this chapter. Mechanism 2 is layer C but originates in Module 9's classification. Mechanisms 3, 4 and 6 are layer C in later chapters. Mechanism 5 is layer B, and distinguishing it matters because it goes to different people with different tools — a bus that never changed ownership is not debugged by reading command traces.

Fix. Per mechanism; and in every case, record the accepted transaction rather than the presented request, so that "no data returned" can be distinguished from "no read was ever issued."

10. Common Misconceptions

"READ returns data immediately."

Why it is tempting: every other bus transaction the learner has met returns in a bounded, often visible, handshake — and a read command looks like a read access.

Concrete failure: a controller model that expects data in the acceptance cycle, or a testbench that checks the data bus one cycle after the command and finds nothing.

Correct model: acceptance starts a timed return. §5's waveform shows seven cycles of pending with nothing on any wire.

Prevention: build the pending record first. A model with a transaction object cannot express "immediately."

"READ activates the row."

Why it is tempting: a read is the operation the user asked for, so it feels like it should do everything needed.

Concrete failure: a read issued to a closed bank. The command is well-formed and the precondition is absent, and on a device the result is not defined.

Correct model: Chapter 9.1 — only an activate opens a row. A read consumes state it did not create.

Prevention: P1. Admission requires a hit, structurally.

"READ carries the full address."

Why it is tempting: it is an addressed operation and it does carry address bits.

Concrete failure: a monitor decodes a row from the column command's operand field — which on a DDR4 read includes the auto-precharge flag — and reports confident nonsense.

Correct model: bank and column on the wire, row in state. Chapter 8.2.

Prevention: capture the row from the bank-state model at acceptance, which §8 requires anyway.

"The read transaction is complete when the command is accepted."

Why it is tempting: acceptance is the last thing the requester's logic does, so it feels terminal.

Concrete failure: a controller that frees the request's resources at acceptance has nothing left to associate the return with, and returned beats arrive with no owner.

Correct model: acceptance creates the transaction. Completion is the final beat, which is Chapter 10.5's.

Prevention: §5's waveform — rd_cmd_valid for one cycle, pending for seven.

"The RTL latency and transaction model is a DRAM device model."

Why it is tempting: it has the right names and behaves plausibly against a testbench.

Concrete failure: an engineer expects the model to catch a timing violation or an array fault, and it cannot — it has no representation of either.

Correct model: layer C only. §6.

Prevention: read the classification header before assigning a block a capability it never claimed.

"A row hit means the read is fast."

Why it is tempting: Chapter 9.3 established a hit as the best case, which invites "best" to become "free."

Concrete failure: a performance model in which hits return immediately, which cannot explain why a 95% hit rate does not deliver peak bandwidth.

Correct model: a hit removes a row-state transition. The read latency is still there, entirely — and Chapter 10.2 is about it.

Prevention: Chapter 9.3 §4's formulation: a hit is the absence of required row-state work, not the absence of an access.

11. Interview Reasoning

"Why must a row already be open before a normal read?"

Because a read names a column within a row that the bank is already holding, and it carries no row address — there is nowhere on the command for one to go. The row was deposited into the bank's state by an earlier activate, so a read consumes state it did not create. The practical consequence is architectural rather than procedural: a read is not a self-contained access, so a controller must model row state to know whether a read is even meaningful, and a read issued to a closed bank is not slow — its precondition is simply absent.

"What does a controller need to track after issuing a read?"

An obligation that nothing on any wire represents. At minimum: an identity, so returning beats can be attributed; the request metadata — bank and column, plus the open-row context captured at acceptance — so that what came back can be compared with what was asked for; and an expectation of when and how much. None of this exists in the DRAM, which was given a command and will produce a burst with no notion of a transaction. The record is created at acceptance and retired at the final beat, and for most of its life there is nothing observable that corresponds to it.

"A read command is on the bus and no data comes back. Where do you look first?"

At whether any data-bus activity happened at all in the expected window, because that single observation splits the problem across a layer boundary. No activity means the device produced nothing — the read was not admitted against an open bank, or the bus never changed ownership — and that is a command-path or PHY question. Activity the controller did not consume means the data was produced and the controller failed to recognise it, which is association, beat counting or the expectation window, and is debugged entirely above the bus with different tools and usually different people.

"Why capture the open-row context when the read is accepted rather than when data returns?"

Because bank state can change during the return latency. The row a read implicitly targets lives in the bank's state, not on the command, so a monitor that resolves it at return time may resolve it against a row that has since been precharged and replaced — and will then compare the returned data against the wrong location in its reference memory. The bug is particularly nasty because it only appears when something closes the row during the window, so it passes on quiet traffic and fails under load.

"Why does this module's first RTL block contain no DQ, no DQS and no data?"

Because it is entirely a controller-side transaction object, and the transaction exists before any data does. Mixing the data path in would blur the boundary the module depends on: the controller does not sample a DRAM cell, the PHY does not know which request it is serving, and the DRAM does not know a transaction exists. Keeping the first block data-free makes the obligation itself visible — a flag and a tag that persist across an interval in which nothing observable happens.

12. Engineering Exercise

NUM_BANKS = 4, COL_W = 10, TAG_W = 2. All banks start closed.

1. A request for bank 1, column 40 arrives while bank 1 is closed. What happens, and what must happen before it can be admitted?

2. Trace pending and pending_tag through: accept · retire · accept · accept · retire · accept.

3. With TAG_W = 2, after how many accepted reads does a tag repeat? Is that an error?

4. A colleague registers rd_cmd_valid to improve timing. Give the failure.

5. A monitor creates an expected transaction for every request with req_valid high. Give the symptom.

6. Write the property that catches metadata changing under an outstanding read, and say what it protects downstream.

13. Summary

Accepting a read returns nothing. It starts a timed return and creates an obligation — a transaction that exists from acceptance until the final beat, with nothing on any wire to represent it for most of that interval.

A read does four things it is commonly believed to do, and none of them. It does not activate a row, does not carry the full address, does not return data in the same cycle, and does not give the controller valid data even when data appears — what appears is captured beats at a boundary.

The record carries identity, metadata and expectation. The device needs none of it: the DRAM was given a command and will produce a burst, with no notion of a requester or a transaction. The transaction is entirely a controller-side construct.

Three layers, kept apart for the whole module. The DRAM command and array; the PHY and electrical transfer; the controller transaction. The controller never samples a cell, the PHY never knows which request it serves, and the DRAM never knows a transaction exists.

Rejections are not one thing. A request refused because a read is already outstanding will succeed unchanged later; a request refused because its row is not open needs a row-state transition first. One signal for both leaves a controller unable to tell wait from fix something.

And the expectation must be built at acceptance. Built from the return instead, it checks the return against itself — and can never detect the most important read failure, which is a read that returns nothing at all.

14. What Comes Next

The pending record now exists and is waiting. Chapter 10.2 — CAS Latency (CL) is about the interval it is waiting through.

Chapter 6.5 already established why an interval exists and why it attached to the column command — that derivation is not repeated. What 10.2 adds is the family of terms built on it: CAS latency, read latency, additive latency and their relationships; why the names differ by generation and what changes with them; and the unit discipline that separates cycles from transfers from a data rate, which is where most latency arithmetic goes wrong.

It also builds the pipeline that carries this chapter's tag across the gap — turning "data comes later" into a model with a configurable, and explicitly educational, depth.


Return to Read (RD / RDA) for the command this chapter begins after, Row Hits for the classification it requires, Row Opening for the state a read consumes, Column Address for why no row travels with a read, and CAS# for why the interval exists at all.

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.