DDR · Module 17
Request Handling
One handshake creates exactly one entry, metadata must hold while stalled, and a read is not complete when its column command issues. Backpressure at the ingress is caused by events many stages downstream.
Four chapters built the machine from the entry pool outward. This one builds the two ends: where requests enter, and where they finish.
Both ends are where controllers meet the rest of the system, and both are where a specific kind of bug lives — not a timing violation the device will reject, but a broken promise to the master upstream. A request accepted twice. A request accepted and lost. A response returned before its data existed.
The chapter's law:
Accepting a request is a contract with three clauses — one handshake creates exactly one entry, the entry is never lost, and the response is not sent until the transaction is genuinely finished. A read is not finished when its column command issues.
1. A Contract, Not a Protocol
The controller's upstream boundary can face many protocols. What it owes is the same in each case, and separating the two is the point.
WHAT THE PROTOCOL DEFINES WHAT THE CONTROLLER OWES
signal names and widths accept at most once
burst and beat encoding never lose an accepted request
response codes respond only when truly done
ordering rules and IDs honour the ordering it accepted
exclusive access semantics apply backpressure truthfullyThe right-hand column does not change if the left-hand column changes. Build the right-hand column as the controller's own contract, and the protocol becomes an adapter in front of it.
2. Ready and Valid, Precisely
The handshake is simple and is very often stated loosely. Stated exactly:
A transfer occurs on a rising clock edge where valid and ready are both high. On that edge, and only on that edge, the receiver takes ownership of the payload.
Four consequences, each of which is a real bug when violated:
valid must not depend combinationally on ready. A producer that asserts valid only when it sees ready creates a combinational loop with a consumer that asserts ready only when it sees valid — and the design either fails to synthesise or, worse, resolves to a state where nothing ever transfers. ready may depend on valid; the asymmetry is the rule.
Once valid is asserted it must remain asserted until a transfer occurs. A producer that withdraws an unaccepted request has changed its mind about something the consumer was entitled to rely on.
The payload must be stable while stalled. If valid is high and ready is low, the address, direction and identity must hold. A producer that rotates through pending requests while waiting — presenting a different one each cycle — will have whichever one happened to be presented when ready rose accepted, and the others silently forgotten.
One transfer, one unit of work. The consumer must not allocate on valid alone, and must not allocate twice for one edge. This is the direct analogue of 17.1 §7's commit discipline at the top of the machine: the handshake is the ingress commit point, and the same class of bug appears if anything acts on intent rather than on the transfer.
cyc valid ready transfer notes
─── ───── ───── ──────── ─────────────────────────────
700 1 0 no stalled — payload must HOLD
701 1 0 no still stalled
702 1 1 YES ownership transfers here
703 0 1 no ready without valid: nothing
704 1 1 YES back-to-back acceptedCycle 703 is worth a glance: ready high with valid low transfers nothing. A receiver that allocates whenever it is ready has allocated an entry containing whatever happened to be on the bus.
3. What Ingress Captures
At acceptance the controller takes ownership and must capture everything it will ever need, because the upstream signals are free to change on the next cycle.
| Captured | Why | Used by |
|---|---|---|
| direction | selects the column command; feeds direction policy | derivation, 17.4 §10 |
| bank / row / column | the decoded target | derivation, legality |
| transaction id | routes the response back | §8's completion |
| burst metadata | how much data, and where it goes | the data path |
The decode. The controller receives a system address and needs a rank, bank group, bank, row and column. This chapter uses the most trivial possible extraction — contiguous bit fields — and labels it as such:
EDUCATIONAL DECODE — a deliberately naive field split.
Module 18 owns address mapping and will show why this
particular arrangement performs poorly.
addr[31:18] row
addr[17:15] bank
addr[14:13] bank group
addr[12:3] column
addr[2:0] byte offset within the beatThis is a placeholder standing in for a decode, not a recommendation. Module 18 shows that the choice of which address bits become the bank index is among the most consequential decisions in the whole memory subsystem, and that a naive contiguous split concentrates traffic in ways that defeat everything Module 16 built.
4. Backpressure Has a Cause
alloc_ready going low is not a random stall. It is the end of a causal chain, and being able to walk that chain is the practical skill this chapter delivers.
a refresh becomes urgent (17.3)
↓
the gate closes; normal issue stops (17.3)
↓
no candidate can commit (17.1)
↓
entries stop advancing (17.2)
↓
no entry reaches DONE; none freed (17.2)
↓
the pool fills (17.2)
↓
alloc_ready deasserts (17.2)
↓
req_ready deasserts (THIS CHAPTER)
↓
the upstream master stallsEvery arrow is a block boundary, and each is individually observable. A stalled master is therefore always explainable, and the explanation is a walk up this chain reading one signal per step.
5. The Ingress Path
The alloc_ready arrow running backwards from the pool to the adapter is the important one. The controller does not compute its own readiness — it forwards the pool's. Inventing a separate ready condition at the boundary means two sources of truth about whether there is room, and they will disagree on the cycle that matters.
6. The Ordering Boundary
Chapter 17.2 §5 established that allocation, scheduling and completion order are three different orders and that the scheduler must be free to let them differ. That raises the obvious question, and this is where it is answered.
If the upstream protocol requires ordering, who enforces it?
The adapter, not the scheduler. And the reason is not layering aesthetics — it is that enforcing ordering inside the scheduler destroys the thing the scheduler exists for.
Three ordering regimes a protocol might impose, named as examples rather than specified here:
- No ordering. Responses in any order, matched by id. The scheduler is unconstrained; nothing extra is needed.
- Ordering within an id or stream, none between. The common case. The adapter tracks outstanding transactions per id and releases in order within each — which needs only per-id buffering, not global.
- Global ordering. Every response in request order. Requires a full reorder buffer, and head-of-line blocking in the response path is now possible independently of anything in the scheduler.
What the controller must never do is accept an ordering obligation it has no mechanism to honour. If the adapter accepts requests under a regime whose ordering it does not enforce, the scheduler will reorder — correctly, by its own contract — and the responses will violate the upstream protocol in a way that looks like a controller bug and is actually a boundary-definition bug.
7. The Ingress Block
// ─────────────────────────────────────────────────────────────────────
// request_ingress_ctrl
//
// CLASSIFICATION
// Synthesizable educational RTL. One responsibility: the controller's
// upstream acceptance contract — one handshake creates exactly one
// entry, backpressure is forwarded truthfully, and a response is
// emitted only for a genuinely completed transaction.
//
// WHAT IT DOES NOT MODEL
// - NOT an AXI or CHI slave. This is a generic ready/valid interface.
// Protocol signalling and ordering belong in an adapter (§1, §6).
// A partial AXI implementation presented as correct would be worse
// than none.
// - No address mapping. §3's decode is deliberately naive and
// Module 18 owns the real thing.
// - No entry storage. Chapter 17.2's pool owns entries; this block
// drives its allocation port.
// - No scheduling, legality, arbitration or commit.
// - No data path. Write data buffering and read data return are a
// separate structure; burst_done is an abstract contract from
// Modules 19-21.
// - No reorder buffer. If the upstream protocol requires ordering,
// §6 puts that in the adapter, not here.
// ─────────────────────────────────────────────────────────────────────
module request_ingress_ctrl #(
parameter int NUM_ENTRIES = 8,
parameter int ADDR_W = 32,
parameter int ID_W = 6,
parameter int ROW_W = 14,
parameter int BK_W = 3,
parameter int BG_W = 2,
parameter int COL_W = 10,
parameter int EN_W = (NUM_ENTRIES <= 1) ? 1 : $clog2(NUM_ENTRIES)
) (
// ── This block is COMBINATIONAL: it holds no state, because the
// state it would otherwise hold already has an owner in Chapter
// 17.2's pool. clk and rst_n are ports so that §10's concurrent
// properties have a sampling event without needing a bind unit,
// and for a sequential extension; the logic below uses neither.
input logic clk,
input logic rst_n,
// ── Upstream, generic ready/valid. §2's contract applies to both
// sides of this interface.
input logic req_valid,
input logic req_is_write,
input logic [ADDR_W-1:0] req_addr,
input logic [ID_W-1:0] req_id,
output logic req_ready,
// ── Downstream: Chapter 17.2's pool allocation port.
input logic alloc_ready,
input logic [EN_W-1:0] alloc_entry,
output logic alloc_en,
output logic alloc_is_write,
output logic [BK_W-1:0] alloc_bank,
output logic [BG_W-1:0] alloc_bg,
output logic [ROW_W-1:0] alloc_row,
output logic [COL_W-1:0] alloc_col,
output logic [ID_W-1:0] alloc_id,
// ── Completion, from the pool. An entry reaching DONE (§8).
input logic done_valid,
input logic [EN_W-1:0] done_entry,
input logic [ID_W-1:0] done_id,
output logic free_en,
output logic [EN_W-1:0] free_entry,
// ── Response, back to the adapter. Also ready/valid: the adapter may
// stall us, for instance while a reorder buffer drains.
output logic rsp_valid,
output logic [ID_W-1:0] rsp_id,
input logic rsp_ready,
// ── Design-error observability.
output logic err_alloc_without_handshake,
output logic err_rsp_without_completion
);
if (NUM_ENTRIES < 1) $fatal(1, "request_ingress_ctrl: NUM_ENTRIES must be >= 1");
if (ADDR_W < (ROW_W + BK_W + BG_W + COL_W + 3))
$fatal(1, "request_ingress_ctrl: ADDR_W too narrow for the field split");
// ── §3's EDUCATIONAL decode. Contiguous fields, chosen for clarity
// and NOT for performance. Module 18 owns the real mapping and
// will show precisely why this arrangement is a poor one.
localparam int COL_LSB = 3;
localparam int BG_LSB = COL_LSB + COL_W;
localparam int BK_LSB = BG_LSB + BG_W;
localparam int ROW_LSB = BK_LSB + BK_W;
assign alloc_col = req_addr[COL_LSB +: COL_W];
assign alloc_bg = req_addr[BG_LSB +: BG_W];
assign alloc_bank= req_addr[BK_LSB +: BK_W];
assign alloc_row = req_addr[ROW_LSB +: ROW_W];
assign alloc_is_write = req_is_write;
assign alloc_id = req_id;
// ── READY IS FORWARDED, NOT INVENTED. §5: a locally computed ready
// is a second source of truth about whether the pool has room.
// Note the direction of dependence -- ready depends on downstream
// readiness, never on req_valid, so no combinational loop can
// form through this block (§2's first consequence).
assign req_ready = alloc_ready;
// ── THE INGRESS COMMIT POINT. Allocation happens on the handshake
// and on nothing else -- not on req_valid, not on alloc_ready.
// This is Chapter 17.1 §7's discipline at the top of the machine.
assign alloc_en = req_valid && req_ready;
assign err_alloc_without_handshake = alloc_en && !(req_valid && req_ready);
// ── Completion. The entry is freed and the response issued together.
// Both are gated on the response being accepted: freeing the entry
// while the response is still stalled would lose the id, which is
// the only thing that can route the response upstream.
logic rsp_fire;
assign rsp_valid = done_valid;
assign rsp_id = done_id;
assign rsp_fire = rsp_valid && rsp_ready;
assign free_en = rsp_fire;
assign free_entry = done_entry;
assign err_rsp_without_completion = rsp_valid && !done_valid;
endmoduleWhy free_en waits for rsp_ready. If the adapter stalls the response — a reorder buffer is full, say — the entry must stay allocated. Freeing it immediately and holding the id in a separate register creates a second place where transaction identity lives, and identity is the one thing that cannot be reconstructed if it is lost. Keeping the entry until the response is accepted means the pool is the single owner of an in-flight transaction from allocation to response.
The cost is real: an entry occupied by a completed transaction is an entry unavailable for new work, so a stalled response path applies backpressure to ingress. That is §4's chain running from the far end, and it is correct — a controller whose responses are not being taken should stop accepting requests.
8. When a Request Is Actually Complete
The most consequential misconception in this module, and the one with the most expensive symptom.
A read is not complete when its RD command issues.
READ
accepted handshake; entry allocated
↓
commands issued PRE / ACT as needed, then RD (17.1 commits)
↓
CAS latency the device is working; nothing to do
↓
burst returns data arrives across several beats (Module 12)
↓
data collected all beats captured and buffered
↓
COMPLETE entry may be freed; response may be sentBetween “RD issued” and “complete” there is a latency and a multi-beat burst. Freeing the entry at column-command issue destroys the destination for data that has not arrived. The data returns anyway — the device was told to send it — and lands with no owner. If the slot has been reallocated, it lands on a different transaction, and the symptom is a read returning another request's data with a plausible id attached.
Writes are different, and the difference is not intuitive.
WRITE
accepted handshake; entry allocated
↓
commands issued PRE / ACT as needed, then WR
↓
write latency CWL elapses
↓
data launched the controller drives the burst (Module 12)
↓
tWR write recovery — the device needs this before
the row may be precharged (Chapter 14.5)
↓
COMPLETE (device) the data is committed to the arrayThe subtlety: when may the write response be sent? That is an upstream protocol question, not a DDR one. Some protocols permit an early response once the write is accepted into the controller and ordering is guaranteed; others require it after the data is committed. The controller must honour whichever it promised — and tWR still governs when the row may be precharged regardless of when the response went out.
The rule the RTL implements: an entry is freed when its burst has completed and its response has been accepted. burst_done comes from the data path, which Modules 19–21 own and this chapter treats as an abstract contract — deliberately, because fabricating a PHY completion model here would make the completion logic look verified when the thing it depends on was invented.
9. Capstone — One Trace Through All Five Chapters
Everything in Module 17 in one run. EDUCATIONAL TIMING — NOT JEDEC VALUES: tRP and tRCD are 3 cycles, CL is 4, the burst is 4 beats, refresh occupancy is 8.
REQUESTS ACCEPTED (17.5)
R0 READ BG0/B0 row 12 entry 0 arrived cyc 800
R1 WRITE BG1/B2 row 7 entry 1 arrived cyc 801
R2 READ BG0/B3 row 4 entry 2 arrived cyc 802
INITIAL BANK STATE (5.2)
B0 OPEN row 9 → R0 is a row CONFLICT
B2 OPEN row 7 → R1 is a row HIT
B3 CLOSED → R2 is a row MISS
REFRESH (17.3)
PENDING, not urgent — normal issue permitted cyc e0 prog e1 prog e2 prog cand(17.1) legal(16.2) gate grant commit effect
─── ──────── ──────── ──────── ────────────── ─────────── ──── ───── ────── ───────────────
803 WAITING WAITING WAITING PRE0 WR1 ACT2 all three ok WR1 YES B2 col issued
804 WAITING DATA WAITING PRE0 - ACT2 both ok ACT2 YES B3 opens row 4
805 WAITING DATA NEEDS_COL PRE0 - - PRE0 only ok PRE0 YES B0 closing
806 NEEDS_ACT DATA NEEDS_COL - - - none ok - no tRP, tRCD
807 NEEDS_ACT DATA NEEDS_COL - - RD2 RD2 ok RD2 YES B3 col issued
808 NEEDS_ACT DONE DATA ACT0 - - ACT0 ok ACT0 YES B0 opens row 12
809 NEEDS_COL free'd DATA - - - none ok - no tRCD
─── REFRESH BECOMES URGENT at cycle 810 ──────────────────────────────────────────────────────
810 NEEDS_COL - DATA RD0 RD0 PREP - no no new rows
811 NEEDS_COL - DATA RD0 PRE0 both PREP PRE B0 YES drain: 17.3 §5
812 NEEDS_COL - DONE RD0 PRE3 both PREP PRE B3 YES drain
813 NEEDS_COL - free'd RD0 RD0 PREP - no all banks idle
814 NEEDS_COL - - - none CLOSED REF YES gate shuts; REF commits
815 NEEDS_COL - - - none BUSY - no occupancy
822 NEEDS_COL - - - none BUSY - no occ_cnt = 7
823 WAITING - - ACT0 - ok - no ← reclassified
824 WAITING - - ACT0 ACT0 ok ACT0 YES B0 reopens row 12Six moments in that trace are the whole module, and the last is the one to take away.
Cycle 803. Three entries, three different command classes as candidates, all legal. 17.4 picks the write because it is the row hit. Allocation order was R0, R1, R2; issue order begins R1 — 17.2 §5's three orders, visible on the first cycle.
Cycle 806. Nothing is legal. Every entry is valid and progressing and the controller issues nothing, correctly. An empty legal mask is a fact about the device, not a failure.
Cycles 808–809. R1 completes and frees at 809, well before R0 which arrived first. Completion order is R1, R2, R0 — the exact reverse of allocation order. If the upstream protocol needs request order, §6's reorder buffer in the adapter is what provides it.
Cycles 810–813 — preparation, which is policy, not legality. Refresh is urgent and 17.3's manager is in DRAIN. The gate is still open: R0's RD is legal and remains a legal candidate throughout. It is simply not selected, because preparing_for_refresh tells the scheduler to stop opening rows and close the ones that are open. That is why cycles 811 and 812 commit PRE commands — the drain is carried out by ordinary normal-issue traffic, and a gate that blocked it would make “all banks idle” unreachable (17.3 §4).
Cycle 814 — now legality changes. All banks are idle, the manager reaches READY, and the gate shuts. R0's RD does not lose an arbitration; it stops being a legal candidate at all, and the legal column goes empty. 17.3 §2: refresh is a gate, not a competitor. The two mechanisms are four cycles apart in this trace, and telling them apart is the difference between debugging a policy and debugging a state machine.
Cycle 823 — the one to take away. Refresh finished and R0's progress reads WAITING, not NEEDS_COL. The refresh closed bank 0, so R0's row 12 is no longer open. Its next command is ACT again, and the work done at cycle 808 has been undone.
10. What the Assertions Prove
// ── P1. Acceptance is exactly the handshake. Both directions: no
// allocation without one, and none missed when one occurs.
property p_alloc_iff_handshake;
@(posedge clk) disable iff (!rst_n)
alloc_en == (req_valid && req_ready);
endproperty
a_alloc_iff_handshake: assert property (p_alloc_iff_handshake);
// ── P2. One handshake, one entry: two consecutive acceptances must
// land in DIFFERENT slots. Catches a pool whose valid bits do not
// take effect in time, and a receiver that allocates on a level
// rather than on the transfer -- both of which put two requests in
// one entry and silently lose the first.
// NOTE ON THE SHAPE. A tempting formulation conjoins alloc_en with
// !$past(req_ready): it is VACUOUSLY TRUE, because alloc_en already
// implies req_ready, so that term is always 0 on the next cycle and
// the property can never fail.
property p_single_allocation;
@(posedge clk) disable iff (!rst_n)
(alloc_en ##1 alloc_en) |-> (alloc_entry != $past(alloc_entry, 1));
endproperty
a_single_allocation: assert property (p_single_allocation);
// ── P3. Payload stability while stalled. §2's third consequence,
// stated as an obligation on the PRODUCER -- so this is an assume
// in a formal run and an assert against a real master.
property p_payload_stable_while_stalled;
@(posedge clk) disable iff (!rst_n)
(req_valid && !req_ready)
|=> (req_valid && $stable(req_addr) && $stable(req_id)
&& $stable(req_is_write));
endproperty
a_payload_stable_while_stalled: assert property (p_payload_stable_while_stalled);
// ── P4. Backpressure is forwarded, not invented. The §5 contract: a
// full pool must be visible upstream on the same cycle.
property p_ready_follows_pool;
@(posedge clk) disable iff (!rst_n)
req_ready == alloc_ready;
endproperty
a_ready_follows_pool: assert property (p_ready_follows_pool);
// ── P5. An entry is freed only when its response was accepted. §8's
// early-free bug, caught at its source rather than by observing
// data land on a reallocated entry several cycles later.
property p_free_only_on_response;
@(posedge clk) disable iff (!rst_n)
free_en |-> (done_valid && rsp_ready);
endproperty
a_free_only_on_response: assert property (p_free_only_on_response);
// ── Covers.
c_stall_then_accept: cover property (@(posedge clk) disable iff (!rst_n)
(req_valid && !req_ready) [*3] ##1 (req_valid && req_ready));
c_rsp_stalled: cover property (@(posedge clk) disable iff (!rst_n)
rsp_valid && !rsp_ready);
c_back_to_back: cover property (@(posedge clk) disable iff (!rst_n)
(req_valid && req_ready) [*2]);What they do not prove. Nothing here shows the response carries the right data — this block routes identity, and the data path is Modules 19–21'. Nothing shows the upstream protocol's ordering is honoured: §6 puts that in the adapter, deliberately outside this block's scope, and a property here would be checking the wrong component. Nothing shows a request eventually completes — that depends on 17.4's fairness and 17.3's progress, neither of which is in scope.
P3 is worth a note on direction. It constrains the producer, so in a formal run it is an assume describing a well-behaved master, and in simulation against a real master it is an assert that catches the master misbehaving. Writing it without deciding which produces either a vacuous property or a false failure.
11. The Independent Model
The model is a transaction ledger keyed by the upstream id, entirely independent of entry indices — so that a pool indexing bug cannot be mirrored.
On each observed handshake, record (id, direction, address, cycle). On each observed response, look up the id, assert it is outstanding, and remove it. At end of test assert the ledger is empty. Separately, reconstruct each transaction's expected command sequence from the address and the observed bank state, and check the observed commands against it.
REQUEST CONTRACT VIOLATION
upstream id : 0x2C
accepted : cycle 1,442 (handshake observed)
accepted AGAIN : cycle 1,443 (second handshake, same id)
outstanding at 1,443 : yes
req_valid at 1,443 : 1
req_ready at 1,442 : 1
req_ready at 1,443 : 1
master withdrew valid : no
diagnosis : the master presented the same id on two consecutive
cycles without withdrawing valid, and the controller
accepted both. Either the master repeated a completed
transaction or it failed to deassert valid after
acceptance.
controller fault? : NO — P1 and P2 both pass. The controller
honoured the handshake exactly twice
because it was offered exactly twice.
action : this is an upstream protocol violation. P3's producer
obligation is the property that names it.The last three lines are why the model belongs at this boundary. The controller is correct, the system is broken, and only a checker that models the contract rather than the implementation can say so. A checker built from the RTL would have reported a clean run.
12. Corner Cases
| Situation | Correct behaviour | Failure if mishandled |
|---|---|---|
valid high, ready low | no allocation; payload must hold | entry allocated with stale fields |
ready high, valid low | nothing happens | entry allocated from bus noise |
| both high, back-to-back | two allocations, two entries | one request merged or duplicated |
| pool full at the handshake cycle | req_ready low; no acceptance | accepted request with nowhere to live |
| response stalled | entry held; ingress backpressures | id lost; response unroutable |
burst_done before response accepted | entry stays in DONE | freed with its response pending |
| completion and allocation same cycle | different entries; both proceed | occupancy drift (17.2 §13) |
NUM_ENTRIES = 1 | EN_W guarded; one outstanding request | zero-width index |
ADDR_W too narrow for the fields | elaboration failure | silently truncated row address |
| reset with a request presented | nothing accepted; no partial entry | phantom entry at reset release |
The ADDR_W guard is the kind of check that looks like defensive clutter and is not. A truncated row field does not fail — it addresses the wrong row, consistently and repeatably, and the symptom is data corruption at a fixed address offset that looks exactly like a mapping bug.
13. Synthesis, Cost and Limits
Cost. Field extraction is wiring. req_ready is a wire from alloc_ready. alloc_en is one AND gate. The entire block is a handful of gates plus the guards, and that is a design goal rather than an accident: the acceptance contract should be cheap enough that there is no temptation to register it, because registering req_ready inserts a cycle of skid that must then be managed with a skid buffer and reintroduces every question §2 settled.
Timing. req_ready is combinational from the pool's alloc_ready, which is combinational from the valid bits. In a deep pool this path can matter — and the fix is worth developing properly, because the naive version of it loses requests.
The obvious repair is to register req_ready. This is wrong, and the reason is exactly §2's contract. A registered ready reports last cycle's capacity. If the pool fills this cycle, req_ready is still high, the master sees a completed handshake and discards its request — and the controller has nowhere to put it. One request, silently gone, under precisely the load that caused the problem.
The correct structure is a skid buffer: one registered slot that absorbs the request in flight when downstream readiness disappears.
cyc req_valid req_ready skid_full alloc_ready action
─── ───────── ───────── ───────── ─────────── ─────────────────────
900 1 1 0 1 accepted → pool
901 1 1 0 0 accepted → SKID
902 1 0 1 0 ready drops; master holds
903 0 0 1 1 skid drains → pool
904 1 1 0 1 accepted → poolCycle 901 is the point. alloc_ready has already gone low, but req_ready was high when the cycle began, so a handshake completes. The request has been accepted and the pool cannot take it — so it lands in the skid slot. Only at 902, one cycle later, does req_ready fall.
So the skid buffer exists to honour a promise the interface already made. Its depth of one is not arbitrary: it holds exactly the number of requests that can be accepted between alloc_ready falling and req_ready responding, which for a single registered stage is one. Registering req_ready without adding the slot is the same design missing the thing that makes it correct — and it is a common enough error to be worth naming.
The cost is one entry of storage, one cycle of latency on a stalled-then-released request, and the bookkeeping to drain the slot before accepting anything new. This block omits it deliberately: the combinational forward of §7 is correct as written, and adding the skid would obscure the contract the chapter exists to teach. A production design at real frequencies will have one.
What a production ingress has that this does not:
- Write data acceptance, which has its own handshake, its own buffering, and its own relationship to the write command's launch timing (Chapter 12.2).
- Read data return buffering and beat assembly.
- The protocol adapter itself — AXI or CHI signalling, burst decomposition, response encoding, ordering enforcement.
- Exclusive access and atomics, which need monitors this block has no concept of.
- Error responses. Every response here is implicitly successful; real controllers report decode errors and, with ECC, uncorrectable errors.
- Quality-of-service class carried through to 17.4's policy.
- Multiple ingress ports with their own arbitration, which is a second, entirely separate arbitration problem from 17.4's.
14. Debugging
Symptom: the upstream master is stalled. Walk §4's chain. One signal per step, three possible origins, and the distinguishing reads are in §4's callout. Do not start at the master.
Symptom: a request was accepted and never completed. §11's ledger names the id and the acceptance cycle. Then ask whether an entry was ever allocated for it — if not, the handshake and the allocation disagree, which P1 catches. If an entry exists, the request is stuck downstream and 17.2 §16's leak report distinguishes a stuck burst from arbitration starvation.
Symptom: the same request is processed twice. Check whether the master withdrew valid after acceptance, as §11's example shows — this is frequently an upstream fault, and P2 plus P3 together attribute it correctly. If the master is well-behaved, the receiver is allocating on a level rather than on the transfer.
Symptom: read data returns for a completed request. §8. The entry was freed before its burst finished. Check free_en against burst_done and the response handshake, which P5 asserts directly.
Symptom: intermittent corruption that correlates with load. The most unpleasant one. Under pressure the pool recycles entries quickly, so an entry freed early is reallocated sooner and the returning data lands on a live transaction rather than a dead one. Low load hides it entirely. Reach for P5 and c_rsp_stalled — if that cover never hit, the stalled-response path has never been exercised.
15. Misconceptions
“Ready/valid means transfer whenever valid is high.” §2 — the transfer is the edge where both are high. Clue: entries allocated with stale or duplicated payloads.
“valid may wait for ready.” That is the combinational loop. ready may depend on valid; not the reverse. Clue: a design that will not synthesise, or an interface that never transfers.
“The controller should compute its own readiness.” §5 — two sources of truth about pool capacity. Clue: an accepted request with nowhere to live.
“Request accepted means request complete.” Acceptance is the start of a sequence that may take many cycles and several commands. Clue: responses far ahead of any possible data.
“A READ completes when the RD command issues.” §8, and the expensive one. Clue: read data with no owner; corruption correlated with pool pressure.
“A WRITE completes when the WR command issues.” Data must still be launched, and tWR still governs precharge. When the response may go out is a protocol question. Clue: a read returning stale data after an acknowledged write.
“Backpressure is an independent signal.” §4 — it is the visible end of a causal chain that starts at the device. Clue: a design that treats a stall as unexplainable.
“The scheduler should preserve request order.” §6 — that switches off the module's entire mechanism. Ordering belongs in the adapter. Clue: bank parallelism that never materialises.
“More outstanding requests always help.” 17.2 §15 — bounded by banks, the activation budget and the mapping. Clue: depth chosen with no reference to any of the three.
16. Interview Reasoning
“When exactly does a transfer occur on a ready/valid interface?” The edge where both are high — then the three obligations: valid may not wait for ready, valid may not be withdrawn, and the payload must hold. Most answers give the first clause and stop.
“Your controller accepted a request and it never completed. Walk me through the debug.” The ledger, then the allocation, then §4's chain. The strong answer separates “never allocated” from “allocated and stuck” before touching the scheduler.
“When is a READ actually complete?” After the burst returns and is collected — and then say what freeing at issue does, and why it is load-dependent.
“The upstream protocol requires in-order responses. What do you change in the scheduler?” Nothing. A reorder buffer in the adapter. Then explain what in-order issue would cost, which is the real question being asked.
“Where does backpressure come from?” Name the chain and the three origins, and note that two of them are correct behaviour and one is a bug.
“How would you verify the acceptance contract?” An independent ledger keyed by upstream id, plus P1 through P5 — and be explicit that P3 constrains the producer, so it is an assume or an assert depending on the run.
“Which parts of what you built are not a real DDR controller?” §13's list, and the equivalent lists in the other four chapters. Knowing the boundary of your own model is the thing being tested.
17. Exercises
1. A master asserts valid, and on the next cycle — still stalled — changes req_addr. Which property fires, and which entry field would have been corrupted had it not?
2. Register req_ready to break the timing path of §13. Describe the request that is now lost, and the smallest structure that prevents the loss.
3. §9's trace: give the cycle at which R0's row is closed by the refresh drain, and the cycle at which its progress is reclassified. Why are they not the same cycle?
4. Rewrite free_en as done_valid alone. Construct the stimulus where a response becomes unroutable, and say which of P1 through P5 catches it.
5. The upstream protocol requires ordering within an id but not between ids. Describe the minimum buffering the adapter needs, and say why it is not a full reorder buffer.
6. ADDR_W is 28 with the §7 field widths. What does the elaboration guard do? Without it, which field is truncated and what is the observable symptom?
7. §11's report shows a double acceptance and concludes the controller is not at fault. Write the property that would make this an upstream failure rather than a silent one, and say whether it is an assert or an assume.
8. Trace §4's chain for a controller where an entry's burst_done never arrives. At which step does the symptom become indistinguishable from a legitimate refresh stall, and which signal separates them?
18. Module 17, Complete
Five chapters, one architecture. Read as a single machine:
A request arrives and is accepted on a handshake, decoded, and allocated into an entry that remembers what it wants (this chapter). Every cycle, every valid entry is compared against live bank state to derive the one command it needs next (17.1) — never recalled, because §9's cycle 823 shows what happens to stored answers. Those candidates are filtered for legality by machinery this module consumed rather than rebuilt (13.1, 16.2) and gated by a refresh manager that turns an obligation into scheduler control (17.3). What survives is chosen by successive narrowing, with a bounded override so that preference cannot starve (17.4). And nothing — no bank state, no timing history, no refresh credit, no entry progress, no rotation pointer — changes until the command commits (17.1 §7).
What Module 17 did not build is worth restating, because the boundary is the professional part: no address mapping, no PHY, no data path, no power management, no ECC, no protocol adapter, no performance claims. Each chapter's limits section names its own omissions, and every one of them is a real component of a production controller.
Module 18 takes up the first of them. The decode in §3 was deliberately naive and labelled as such; address mapping determines whether the bank parallelism Module 16 measured and the scheduler this module built ever have independent work to find. A perfect scheduler on a mapping that concentrates traffic into one bank is a perfect scheduler with nothing to schedule.
Continue learning
Related tutorials
- 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
The Command Scheduler
A DDR command scheduler does not schedule requests. It re-derives each outstanding request's next required command every cycle, and advances architectural state only at the commit point where a command is genuinely issued.
- Related topic
The Command Queue
The structure every controller calls a command queue does not hold commands, is usually not per bank, and is not a queue. Allocation order, scheduling order and completion order are three different orders.
- Related topic
The Refresh Manager
Refresh due, refresh legal, refresh issued and refresh complete are four distinct events separated by many cycles. The manager turns an obligation into the drain, the gate and the request a scheduler consumes.
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.
