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:
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 DRAMAnd 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.
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
// ─────────────────────────────────────────────────────────────────────────
// 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
endmoduleState representation and transitions
One pending bit, one tag, and the request's metadata. The transition graph is two edges:
idle --req_accept--> pending(tag, bank, col)
pending --retire--> idlereq_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
| Cycle | Input | req_accept | pending after | pending_tag | Reject |
|---|---|---|---|---|---|
| 0 | idle | 0 | 0 | — | — |
| 1 | req_valid, b1, c40, HIT | 1 | 1 | 0 | — |
| 2 | req_valid, b1, c41, HIT | 0 | 1 | 0 | busy |
| 3 | idle | 0 | 1 | 0 | — |
| 4 | retire | 0 | 0 | 0 | — |
| 5 | req_valid, b2, c10, HIT | 1 | 1 | 1 | — |
| 6 | req_valid, b3, c00, MISS | 0 | 1 | 1 | not_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 cyclesrd_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.
| Layer | Owns | Speaks in terms of |
|---|---|---|
| A — DRAM command and array | the open row, column selection, internal data movement | banks, rows, columns, commands |
| B — PHY and electrical transfer | bus ownership, the strobe, source-synchronous capture | DQ, DQS, beats, edges |
| C — Controller transaction | admission, pending records, association, completion | requests, 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 interface — Chapter 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
// 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:
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, retireFour 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.
- The read was never actually admitted — the command on the interface came from somewhere else, or the request was rejected and the rejection ignored.
- The read was admitted against a bank that was not actually open, so the device received a column command it could not serve.
- Data returned and was not recognised — the return path, the beat count, or the association failed. Chapters 10.3 and 10.4.
- Data returned at a different time than expected and the expectation window had closed. Chapter 10.2.
- The bus never changed ownership, so the device drove nothing. Chapter 10.3.
retireis 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_acceptever high for this request? If not, mechanism 1, andreject_not_hitversusreject_busysays which. This is one signal and it eliminates the most. - Was the bank in
BANK_OPENat 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
pendingstill 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
Related tutorials
- Related topic
The Write Command
Accepting a read creates an obligation to recognise something. Accepting a write creates an obligation to produce something — and that single reversal explains almost every way writes differ from reads.
- Related topic
Read (RD / RDA)
READ names a column in a row that is already open — it opens nothing. And RDA is not a separate command but a READ carrying one address bit high, which is why a decoder must identify the operation before interpreting any operand.
- Related topic
Write (WR / WRA)
A write's data arrives after its command, which makes three events impossible to conflate: observed, accepted, and completed. A monitor, a protocol checker and a scoreboard each attach to a different one.
- Related topic
Precharge (PRE / PREA)
Precharge closes a bank, and its scope is decided by an operand: one bank or all of them. It is also the command that exposes why a command stream does not fully describe device state — which is the hardest problem a DV monitor faces.
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.
