DDR · Module 29
AXI-to-DDR Flow
One AXI transaction is not one DDR command, acceptance is not service, and the ordering the bus guarantees is not the ordering the scheduler uses. This is the transformation between them.
A DDR controller never sees a CPU load. It never sees a DMA transfer or an AI tensor. It sees memory requests that have already passed through several SoC layers, each of which changed what the request means.
This module's central law:
AN SoC REQUEST IS NOT A DDR COMMAND.
Between the two sit an address decode, a queue, an address map, a row-state classification, an arbiter and a timing-legality check — and each can change the count, the identity, and the order of what arrives.
This chapter is that transformation, and it is deliberately narrow: not what AXI is, not how a controller schedules, but what happens to a request's count, identity and order as it crosses the boundary between them.
1. Four Different Things Called "A Request"
The vocabulary problem comes first, because the whole chapter is about quantities that are equal only by accident.
| Object | Lives on | Identified by | Owned by |
|---|---|---|---|
| Bus transaction | the interconnect | a bus ID plus its own handshake | 5.1, 8.3 |
| Controller request | the controller's queue | an internal tag | 17.2 |
| DDR command | the DRAM interface | nothing — it is an event | Module 7 |
| Data beat | the data bus | its position in a burst | 12.1 |
None of the four maps one-to-one onto any other, and treating any pair as equal produces a specific wrong prediction:
| Assumed equality | What it predicts | Why it is wrong |
|---|---|---|
| bus transaction = controller request | one AXI read → one queue entry | §4 — boundary splitting |
| controller request = DDR command | one queue entry → one RD | §3 — a request may need PRE + ACT + RD |
| bus burst = DDR burst | AXI length = DDR burst length | §5 — independent parameters |
| acceptance = service | ARREADY high → data is coming from DRAM | §2 — the request has not even been mapped yet |
CURRICULUM-DERIVED from 17.2, which owns the distinction between allocation, scheduling and completion: those three are already distinct inside the controller. This chapter adds the layer above, where a fifth distinction appears — the bus transaction, which exists before allocation.
2. Acceptance Is Not Service
The most consequential misreading in SoC memory work, and the cheapest to correct.
An AXI handshake is an agreement to take the request. Chapter 3.1 owns the handshake and 17.5 owns the controller side of it — the one-handshake-one-entry rule and metadata stability while stalled.
What acceptance establishes: the request is now the controller's responsibility, the requester may release its buffer, and the requester may issue another.
What acceptance does not establish: that any DRAM activity has begun, will begin soon, or will begin in this order.
what has happened at the instant ARVALID && ARREADY
done:
the address, length, size, burst type and ID are captured
an entry exists in the controller's queue (17.2)
the requester's outstanding count has increased (8.1)
NOT done:
the address has not been mapped to bank/row/column (8.6)
the row state is unknown to the scheduler yet (Module 9)
no timing legality has been evaluated (Module 14)
no command has been committed (17.1)
nothing has been reordered yet -- or it has, and this
request may be behind requests accepted after itSo the gap between acceptance and the first DDR command is unbounded in principle and variable in practice. Chapter 17.4 owns the arbitration that decides, and 23.4 owns the reordering heuristics that may deliberately serve a later request first.
And this is why a full request queue does not mean DRAM is busy. The queue can be full of requests to one bank that is waiting on tRC while the data bus sits idle — or full because an upstream burst arrived faster than the scheduler could drain it. Queue occupancy measures the gap between arrival rate and service rate, not DRAM utilisation, and §17 treats the two as separate measurements.
3. Why One Request Is Several Commands
Chapter 17.1 owns the request-to-command expansion. What matters here is that the expansion factor depends on state the requester cannot see.
CURRICULUM-DERIVED from Module 9, which owns row hits, misses and conflicts:
| Row state when the request is scheduled | Commands required | Owner |
|---|---|---|
| The target row is already open | RD alone | 9.3 |
| The bank is idle | ACT then RD | 9.4 |
| A different row is open in that bank | PRE, ACT, then RD | 9.5 |
One to three commands for an identical request, decided by what happened to that bank beforehand. So the command count is a property of the request stream's history, not of the request — which is the first hint of why 29.3 through 29.5 exist: the shape of the stream determines the expansion factor.
And the page policy changes it again. Chapter 23.5 owns open-page, close-page and adaptive policies and the crossover between them: under a close-page policy the row is precharged after use, so a subsequent access to the same row pays ACT again. The same request, the same row, a different policy, a different command count.
4. The Count Problem — Splitting and Merging
A single bus transaction can become several controller requests, and several can become one.
Splitting happens for reasons the requester did not choose.
| Cause | Mechanism | Owner |
|---|---|---|
| A burst crosses a controller or channel boundary | the decode routes halves to different destinations | 12.3 |
| A burst is longer than the controller's request granularity | the front end splits to its internal size | 17.5 |
| Data-width conversion | a wide beat becomes several narrow ones, or the reverse | 7.7 |
| A burst crosses a DRAM row boundary | the second part needs a different row — 8.6 | Module 18 |
Row-boundary splitting is the one that surprises people, because it is invisible at the bus. A perfectly legal, perfectly aligned INCR burst can straddle a row boundary, and the halves then require different ACTs — one bus transaction, two rows, potentially two row conflicts.
Merging happens too, and it is a policy rather than a necessity. A controller may combine writes to the same row, or absorb a write into a pending write to the same address. CURRICULUM-DERIVED from 17.2: whether it does is an implementation choice, and the queue entry's fields determine what merging is even possible.
So the honest arithmetic is a chain of independent factors, not a formula:
one bus transaction
x splits from decode, granularity, width, row boundary
= N controller requests
x 1 to 3 commands each, depending on row state and policy
= M DDR commands
+ refresh commands interleaved by the manager (17.3)
N and M are NOT derivable from the transaction alone. They depend
on the address map, the mapping policy, the row state left by
earlier traffic, and the page policy.
this is why "how many DDR commands will my burst cost?" has no
answer without the stream it sits in.5. Bus Burst Length Is Not DDR Burst Length
A specific case of §4 that deserves its own statement because the two use the same word.
CURRICULUM-DERIVED: Chapter 7.1 owns bus burst length, size and beats. Chapter 12.1 owns the DDR burst length and the generation-specific burst contract. They are independent parameters chosen by different designers for different reasons.
| Bus burst | DDR burst | |
|---|---|---|
| Set by | the requester, per transaction | the device generation and mode — 12.1 |
| Units | beats of the bus data width | transfers on the DQ bus |
| Varies per transaction | yes | no — it is a device property |
| Owner | 7.1 | 12.1 |
So the relationship between them is arithmetic across two different widths, and it is not a passthrough. A request of a given byte count becomes some number of DDR bursts determined by the DDR burst length and the interface width — and if the byte count is not a multiple of that product, the last burst is partially masked (6.11 owns the data mask) or the access is split.
The practical consequence is that "use longer bursts" is advice about the bus, not about DRAM. Chapter 19.2 owns that advice from the AXI side and it is sound: longer bus bursts amortise bus-side overhead. What they do for DRAM is different and indirect — a longer burst covers more consecutive columns in one row, which raises the row-hit rate, which is 23.3's subject. The benefit is a locality benefit, not a burst-length benefit.
6. The Identity Problem
Requests must be matched to responses across a boundary where reordering is legal on both sides, for different reasons.
CURRICULUM-DERIVED from 8.3 and 8.4: the bus permits out-of-order completion between different IDs and requires ordering within one ID. The controller's reordering is governed by a different rule — 23.4 owns the heuristics, and they reorder for row locality and bank availability, with no knowledge of bus IDs unless someone wired that knowledge in.
So a controller front end has an obligation the scheduler does not know about.
| Constraint | Source | Who must enforce it |
|---|---|---|
| Same-ID responses in order | 8.3 | the controller front end |
| Different-ID responses may complete out of order | 8.4 | nobody — it is permission |
| Split parts of one transaction recombine in order | the transaction's own semantics | the front end |
| Row-locality reordering | 23.4 | the scheduler, freely |
Row one and row four are in tension, and the front end is where it is resolved. A scheduler free to reorder will return data for two same-ID requests in the wrong order; something must hold the later one back. §11's bookkeeping is that something, and the cost is a reorder buffer whose depth bounds how much reordering the scheduler can be allowed to do.
This is the sharpest architectural point in the chapter. The ordering the bus guarantees and the ordering the scheduler wants are different, and the boundary between them is not free — it costs storage, and the storage bounds the performance benefit. Chapter 17.5 owns the ordering boundary between the controller and whatever the upstream protocol requires, and this is what that boundary costs.
7. The SoC Path
8. One Request, End to End
Two things the sequence is built to show.
The second message is a dead end on purpose. Acceptance returns to the requester and nothing downstream has happened yet — §2's argument drawn as a message that goes nowhere useful.
And candidate — not a command. Chapter 17.1 owns the commit point as the single instant architectural state may change, and the self-loop on the scheduler is where one to three commands are decided. Between the queue and the device there is no one-to-one arrow, which is §3.
9. A Worked Trace
One request, followed concretely. All values ILLUSTRATIVE — a 64-byte request, a 32-bit-wide DRAM interface, DDR burst length 8, and a mapping with the column field in the low bits. Real systems differ in every one of these.
REQUEST read, 64 bytes, aligned, ID = 5
arrives cycle 100 (controller clock), accepted same cycle
---- step 1: acceptance (17.5) ----
cycle 100 handshake completes; queue entry allocated
fields retained: addr, 64B length, ID=5
requester's outstanding count += 1
DRAM activity so far: NONE
---- step 2: address mapping (8.6) ----
physical addr -> rank 0, bank group 1, bank 5,
row 0x1A40, column 0x080
DERIVED under the ILLUSTRATIVE map above; a different
mapping policy (18.1) gives a different bank and row
for the SAME address.
---- step 3: how many DDR bursts? ----
64 bytes / (32 bits x 8 transfers) = 64 / 32 = 2
so this ONE request needs TWO DDR read bursts
at consecutive columns in one row.
note: 2, not 1, and not 8. The factor comes from the
interface width and the device burst length -- 12.1 --
neither of which the requester chose.
---- step 4: row state (Module 9) ----
case A row 0x1A40 already open in bank 5 -> RD, RD
case B bank 5 idle -> ACT, RD, RD
case C row 0x0C12 open in bank 5 -> PRE, ACT, RD, RD
---- step 5: timing legality (Module 14) ----
case B ACT at cycle 104 (after arbitration)
first RD no earlier than ACT + tRCD (14.1)
second RD no earlier than first + tCCD (14.6)
---- step 6: data return ----
first beats arrive CL after the first RD (14.9)
the two bursts return back-to-back if tCCD allows
---- step 7: response ----
front end reassembles both bursts into one response
and returns it; if another ID=5 read was accepted
EARLIER and is still outstanding, this response WAITS
-- 8.3's same-ID ordering rule, enforced by §11's
bookkeeping, not by the scheduler.
TOTALS for one 64-byte request:
controller requests : 1 (no split -- it fits in one row here)
DDR commands : 2 to 4, depending on row state
data bursts : 2
and the response may be held behind an earlier same-ID requestThe arithmetic worth remembering is step 3: one request, two bursts. That factor is set by the interface width and the device burst length, and the requester chose neither. A designer reasoning about "one transaction" is reasoning about an object that does not exist at the DRAM interface.
And step 4's three cases are the whole of 29.3 onward in miniature. The same request costs two, three or four commands depending on what the previous traffic left in bank 5 — so the cost of a request is a property of the stream, and shaping the stream is the only lever a requester has.
10. The Request Descriptor and the Splitter
// ---------------------------------------------------------------------
// soc_req_splitter -- CLASSIFICATION: synthesisable, ILLUSTRATIVE.
//
// WHAT IT DOES: turns ONE accepted bus read transaction into N internal
// requests, where N is decided by a boundary the requester did not
// choose (§4). Each part gets its own internal tag; all parts keep the
// original bus ID, because 8.3's same-ID ordering rule is about the ID
// and the parts must recombine in order.
//
// WHY IT EXISTS: §1's table. "One transaction" and "one controller
// request" are different objects, and the factor between them comes
// from the address map, the controller's granularity and a row
// boundary -- none of which the requester controls.
//
// WHY ACCEPTANCE IS SEPARATED FROM ISSUE (§2): this block does NOT
// drive a DDR command. It produces internal requests that a scheduler
// will later choose among. A block that asserted a DDR read on the
// cycle the handshake completed would teach a false contract: there is
// no such path in any real controller.
//
// NOT AN AXI SLAVE: no channel FSMs, no write path, no WSTRB, no
// narrow transfers, no error responses. The AXI track owns those.
//
// SYNTHESIS: one adder, one comparator, a small counter. No memory.
// ---------------------------------------------------------------------
module soc_req_splitter #(
parameter int ADDR_W = 40,
parameter int ID_W = 6,
parameter int TAG_W = 8,
parameter int LEN_W = 12, // bytes, ILLUSTRATIVE encoding
parameter int BOUND_LOG = 10 // split boundary = 2**BOUND_LOG bytes
)(
input logic clk,
input logic rst_n,
// ---- an ALREADY-ACCEPTED transaction (the handshake happened
// upstream; 17.5 owns that contract, this block consumes its result)
input logic req_valid,
output logic req_ready,
input logic [ADDR_W-1:0] req_addr,
input logic [LEN_W-1:0] req_bytes,
input logic [ID_W-1:0] req_id,
// ---- internal requests, one per part
output logic part_valid,
input logic part_ready,
output logic [ADDR_W-1:0] part_addr,
output logic [LEN_W-1:0] part_bytes,
output logic [ID_W-1:0] part_bus_id, // preserved, §6
output logic [TAG_W-1:0] part_tag, // allocated per part
output logic part_first,
output logic part_last,
// ---- published accounting
output logic [15:0] txns_accepted,
output logic [15:0] parts_emitted,
output logic [15:0] splits_occurred
);
// $clog2 is not used for an index here, but the parameter bounds
// still need stating: a zero-length request and a degenerate
// boundary are both silently wrong rather than loudly wrong.
initial begin
if (ADDR_W < 8) $fatal(1, "soc_req_splitter: ADDR_W must be >= 8");
if (ID_W < 1) $fatal(1, "soc_req_splitter: ID_W must be >= 1");
if (TAG_W < 1) $fatal(1, "soc_req_splitter: TAG_W must be >= 1");
if (LEN_W < 2) $fatal(1, "soc_req_splitter: LEN_W must be >= 2");
// A boundary below 2 bytes cannot split anything meaningfully, and
// one at or above the address width can never be crossed.
if (BOUND_LOG < 1 || BOUND_LOG >= ADDR_W)
$fatal(1, "soc_req_splitter: BOUND_LOG must be in 1..ADDR_W-1");
end
localparam logic [ADDR_W-1:0] BOUND_MASK = (({{(ADDR_W-1){1'b0}}, 1'b1}) << BOUND_LOG) - 1;
logic busy;
logic [ADDR_W-1:0] cur_addr;
logic [LEN_W-1:0] cur_left;
logic [ID_W-1:0] cur_id;
logic [TAG_W-1:0] next_tag;
logic cur_first;
// Bytes remaining to the next boundary from the current address.
// This is the whole of §4: the split point is a property of the
// ADDRESS, not of the request.
logic [ADDR_W-1:0] to_boundary;
always_comb begin
to_boundary = (BOUND_MASK - (cur_addr & BOUND_MASK)) + 1'b1;
end
// This part is the smaller of "what is left" and "what fits before
// the boundary". Comparing in the wider address width avoids a
// truncation that would silently emit an oversized part.
logic [LEN_W-1:0] this_bytes;
always_comb begin
this_bytes = ({{(ADDR_W-LEN_W){1'b0}}, cur_left} <= to_boundary)
? cur_left
: to_boundary[LEN_W-1:0];
end
assign req_ready = !busy;
assign part_valid = busy;
assign part_addr = cur_addr;
assign part_bytes = this_bytes;
assign part_bus_id = cur_id;
assign part_tag = next_tag;
assign part_first = cur_first;
// `last` is computed from what remains AFTER this part, so a
// single-part request is both first and last -- which is what a
// downstream reassembler needs to complete it immediately.
assign part_last = (this_bytes == cur_left);
always_ff @(posedge clk) begin
if (!rst_n) begin
busy <= 1'b0;
cur_addr <= '0;
cur_left <= '0;
cur_id <= '0;
next_tag <= '0;
cur_first <= 1'b0;
txns_accepted <= '0;
parts_emitted <= '0;
splits_occurred <= '0;
end else begin
// Accept a new transaction only when idle. 17.5's
// one-handshake-one-entry rule: this block must not take a
// second transaction while still emitting the first's parts, or
// the parts of two transactions would interleave and 8.3's
// ordering rule would be unenforceable downstream.
if (req_valid && req_ready) begin
// A zero-byte request would make cur_left zero and emit a
// part with this_bytes == 0 forever. Refused by construction:
// busy is only set for a non-zero length.
if (req_bytes != '0) begin
busy <= 1'b1;
cur_addr <= req_addr;
cur_left <= req_bytes;
cur_id <= req_id;
cur_first <= 1'b1;
if (txns_accepted != 16'hFFFF) txns_accepted <= txns_accepted + 1'b1;
end
end
if (part_valid && part_ready) begin
if (parts_emitted != 16'hFFFF) parts_emitted <= parts_emitted + 1'b1;
next_tag <= next_tag + 1'b1; // wraps; TAG_W bounds outstanding
cur_first <= 1'b0;
if (part_last) begin
busy <= 1'b0;
cur_left <= '0;
end else begin
// Advance to the boundary and continue. Counting a split
// here rather than at accept time means the number reflects
// splits that actually happened.
cur_addr <= cur_addr + this_bytes;
cur_left <= cur_left - this_bytes;
if (splits_occurred != 16'hFFFF) splits_occurred <= splits_occurred + 1'b1;
end
end
end
end
endmodulereq_ready is low while parts are being emitted, and that is 17.5's rule enforced. Chapter 17.5 owns one-handshake-one-entry; if this block accepted a second transaction mid-split, the parts of two transactions would interleave and no downstream reassembler could honour 8.3's same-ID ordering. The cost is that a splitting request blocks acceptance, which is a real design trade and the reason a production front end pipelines this.
part_bus_id is carried and part_tag is allocated. The bus ID is the upstream ordering key (8.3); the tag is the internal identity used to match a returning burst. They are different namespaces and conflating them is §6's defect — a controller that reordered by bus ID would serialise unrelated requesters, and one that returned responses by internal tag would violate same-ID ordering.
And to_boundary is computed from the address, not the length. §4's point in one expression: the split point is a property of where the request sits, which is why an aligned, legal burst can still split.
11. Outstanding and Response Bookkeeping
// ---------------------------------------------------------------------
// resp_reorder_tracker -- CLASSIFICATION: synthesisable, ILLUSTRATIVE.
//
// WHAT IT DOES: §6's reconciliation. The scheduler may complete
// internal requests in any order (23.4 reorders for row locality).
// This block releases responses so that:
// - all parts of one transaction complete before it responds (§4)
// - same-ID transactions respond in allocation order (AXI 8.3)
// - different-ID transactions may respond out of order (AXI 8.4)
//
// WHY THE DEPTH MATTERS: this is the storage cost of the ordering
// boundary 17.5 owns. A shallow tracker forces the scheduler to
// reorder less; a deep one costs area. The trade is architectural,
// not a tuning knob.
//
// WHAT WAS REMOVED: the data path. A real reorder buffer also stores
// the returned beats, and that storage -- not this bookkeeping --
// dominates its area.
//
// SYNTHESIS: a small register file plus priority encoders. Depth is a
// parameter because it is the architectural dial.
// ---------------------------------------------------------------------
module resp_reorder_tracker #(
parameter int DEPTH = 16,
parameter int ID_W = 6,
parameter int TAG_W = 8
)(
input logic clk,
input logic rst_n,
// ---- allocation: one per internal request emitted by §10
input logic alloc_valid,
output logic alloc_ready,
input logic [ID_W-1:0] alloc_bus_id,
input logic [TAG_W-1:0] alloc_tag,
input logic alloc_last, // last part of its transaction
// ---- completion: out of order, from the scheduler/data path
input logic done_valid,
input logic [TAG_W-1:0] done_tag,
// ---- response release, ordering-correct
output logic resp_valid,
input logic resp_ready,
output logic [ID_W-1:0] resp_bus_id,
// ---- published accounting
output logic [$clog2(DEPTH+1)-1:0] occupancy,
output logic [15:0] allocated,
output logic [15:0] completed,
output logic [15:0] released,
output logic [15:0] unmatched_done,
output logic full
);
initial begin
if (DEPTH < 2) $fatal(1, "resp_reorder_tracker: DEPTH must be >= 2 (got %0d)", DEPTH);
if (ID_W < 1) $fatal(1, "resp_reorder_tracker: ID_W must be >= 1");
if (TAG_W < 1) $fatal(1, "resp_reorder_tracker: TAG_W must be >= 1");
// DEPTH entries need a tag space at least as large, or two live
// entries could share a tag and a completion would match both.
if ((1 << TAG_W) < DEPTH)
$fatal(1, "resp_reorder_tracker: TAG_W too narrow for DEPTH");
end
// Allocation order is the release order WITHIN an ID, so entries are
// held in an age-ordered array: index 0 is the oldest.
logic v [DEPTH];
logic [ID_W-1:0] id_q [DEPTH];
logic [TAG_W-1:0] tag_q [DEPTH];
logic lastq [DEPTH];
logic donq [DEPTH];
logic [$clog2(DEPTH+1)-1:0] count;
assign occupancy = count;
assign full = (count == DEPTH[$clog2(DEPTH+1)-1:0]);
assign alloc_ready = !full;
// The head is releasable when it is valid, complete, and is the last
// part of its transaction. Releasing from the HEAD only is what makes
// same-ID ordering automatic: an older entry with the same ID is
// always nearer the head, so it releases first.
//
// This is deliberately stricter than AXI requires -- 8.4 permits
// different IDs to complete out of order, and head-only release
// serialises them. A production design tracks per-ID chains so
// unrelated IDs are not held up; that is more logic and the same
// contract. Stated here rather than hidden.
logic head_releasable;
assign head_releasable = v[0] && donq[0] && lastq[0];
assign resp_valid = head_releasable;
assign resp_bus_id = id_q[0];
always_ff @(posedge clk) begin
if (!rst_n) begin
count <= '0;
allocated <= '0;
completed <= '0;
released <= '0;
unmatched_done <= '0;
for (int i = 0; i < DEPTH; i++) begin
v[i] <= 1'b0; id_q[i] <= '0; tag_q[i] <= '0;
lastq[i] <= 1'b0; donq[i] <= 1'b0;
end
end else begin
// ---- completion first, so a completion arriving in the same
// cycle as a release is applied to the pre-shift array. Doing it
// after the shift would mark the WRONG entry -- the stale-index
// hazard.
if (done_valid) begin
automatic bit hit = 1'b0;
for (int i = 0; i < DEPTH; i++)
if (v[i] && tag_q[i] == done_tag && !donq[i]) begin
donq[i] <= 1'b1;
hit = 1'b1;
end
if (hit) begin
if (completed != 16'hFFFF) completed <= completed + 1'b1;
end else begin
// A completion with no matching outstanding entry is a real
// finding, not a no-op: it means a response exists for a
// request nobody allocated.
if (unmatched_done != 16'hFFFF) unmatched_done <= unmatched_done + 1'b1;
end
end
// ---- release and compact. Shifting on release keeps index 0 as
// the oldest, which is what makes same-ID ordering structural
// rather than checked.
if (head_releasable && resp_ready) begin
for (int i = 0; i < DEPTH - 1; i++) begin
v[i] <= v[i+1];
id_q[i] <= id_q[i+1];
tag_q[i] <= tag_q[i+1];
lastq[i] <= lastq[i+1];
donq[i] <= donq[i+1];
end
v[DEPTH-1] <= 1'b0;
if (count != '0) count <= count - 1'b1;
if (released != 16'hFFFF) released <= released + 1'b1;
end
// ---- allocate into the tail. When a release happens in the
// same cycle the array shifts down by one, so the new tail index
// is count-1 rather than count.
if (alloc_valid && alloc_ready) begin
automatic int idx = (head_releasable && resp_ready && count != 0)
? int'(count) - 1 : int'(count);
v[idx] <= 1'b1;
id_q[idx] <= alloc_bus_id;
tag_q[idx] <= alloc_tag;
lastq[idx] <= alloc_last;
donq[idx] <= 1'b0;
if (!(head_releasable && resp_ready)) begin
if (count != DEPTH[$clog2(DEPTH+1)-1:0]) count <= count + 1'b1;
end
if (allocated != 16'hFFFF) allocated <= allocated + 1'b1;
end
end
end
endmoduleHead-only release is stated as stricter than the protocol requires, and that honesty matters. Chapter 8.4 permits different IDs to complete out of order; this block serialises them. A production front end tracks per-ID chains so an unrelated ID is not held behind a slow one — more logic, the same contract. Teaching the simplification without naming it would teach a false performance model.
Completion is applied before the shift, deliberately. Both happen in one clocked block, so a completion arriving in the same cycle as a release must mark the entry at its pre-shift index. Applying it after would mark the wrong entry — the stale-index hazard, and it would silently complete a different request.
And unmatched_done is counted rather than ignored. A completion with no outstanding entry means a response exists for a request nobody allocated. That is a loss-of-accounting finding, and §13's properties treat it as one.
12. What the Assertions Prove
// Bound to §10's splitter and §11's tracker. Every property carries
// `disable iff (!rst_n)`, and every antecedent is covered below.
//
// A NOTE ON LIVENESS: there is deliberately no property here asserting
// that an accepted request is EVENTUALLY served. Such a property needs
// an environmental assumption -- that the scheduler is given
// opportunities, that refresh does not monopolise the device, that
// downstream accepts responses -- and an unbounded liveness assertion
// without those assumptions fails on a legal-but-slow system. What is
// asserted instead is ACCOUNTING: nothing is lost, nothing is
// duplicated, and no response exists without a request.
module axi_to_ddr_sva #(
parameter int DEPTH = 16, parameter int ID_W = 6, parameter int TAG_W = 8
)(
input logic clk, rst_n,
input logic req_valid, req_ready, part_valid, part_ready, part_first, part_last,
input logic [ID_W-1:0] req_id, part_bus_id, resp_bus_id,
input logic [TAG_W-1:0] part_tag, done_tag,
input logic [11:0] req_bytes, part_bytes,
input logic [15:0] txns_accepted, parts_emitted, splits_occurred,
input logic alloc_valid, alloc_ready, done_valid, resp_valid, resp_ready, full,
input logic [15:0] allocated, completed, released, unmatched_done,
input logic [$clog2(DEPTH+1)-1:0] occupancy
);
// ---- P1. FORBIDDEN. No new transaction is accepted while parts of
// the previous one are still being emitted. 17.5's
// one-handshake-one-entry rule: interleaved parts would make 8.3's
// same-ID ordering unenforceable downstream.
property p_no_accept_mid_split;
@(posedge clk) disable iff (!rst_n)
(part_valid && !part_last) |-> !req_ready;
endproperty
assert property (p_no_accept_mid_split)
else $error("splitter: accepted a transaction mid-split");
// ---- P2. Every emitted part carries the ORIGINAL bus ID. §6: the
// bus ID is the upstream ordering key and must survive splitting.
property p_bus_id_preserved;
@(posedge clk) disable iff (!rst_n)
(req_valid && req_ready && req_bytes != 0)
##1 (part_valid [->1]) |-> (part_bus_id == $past(req_id, 1));
endproperty
assert property (p_bus_id_preserved)
else $error("splitter: bus ID not preserved across the split");
// ---- P3. FORBIDDEN. A part is never larger than the request. A
// part exceeding the original byte count means the boundary
// arithmetic truncated.
property p_part_fits;
@(posedge clk) disable iff (!rst_n)
part_valid |-> (part_bytes != 0);
endproperty
assert property (p_part_fits)
else $error("splitter: emitted a zero-length part");
// ---- P4. Parts are emitted at least as often as transactions are
// accepted: one transaction yields one or more parts, never zero.
property p_parts_at_least_txns;
@(posedge clk) disable iff (!rst_n)
(parts_emitted >= txns_accepted) || (txns_accepted == 16'd0);
endproperty
assert property (p_parts_at_least_txns)
else $error("splitter: %0d parts for %0d transactions", parts_emitted, txns_accepted);
// ---- P5. §4's accounting identity. Every emitted part is either
// the last of its transaction or was preceded by a split, so
// parts = transactions + splits exactly.
property p_split_accounting;
@(posedge clk) disable iff (!rst_n)
(parts_emitted == txns_accepted + splits_occurred)
|| (parts_emitted == 16'hFFFF) || (txns_accepted == 16'hFFFF);
endproperty
assert property (p_split_accounting)
else $error("splitter: %0d parts != %0d txns + %0d splits",
parts_emitted, txns_accepted, splits_occurred);
// ---- P6. FORBIDDEN. `first` and `last` are both set only for a
// single-part transaction, never mid-stream.
property p_first_last_consistent;
@(posedge clk) disable iff (!rst_n)
(part_valid && part_first && !part_last) |-> (part_bytes != 0);
endproperty
assert property (p_first_last_consistent)
else $error("splitter: inconsistent first/last on a part");
// ---- P7. FORBIDDEN. Allocation is refused when full. An allocation
// into a full tracker overwrites a live entry and loses a request.
property p_no_alloc_when_full;
@(posedge clk) disable iff (!rst_n)
full |-> !alloc_ready;
endproperty
assert property (p_no_alloc_when_full)
else $error("tracker: allocation accepted while full");
// ---- P8. INVARIANT. Occupancy never exceeds the depth. The counter
// is sized for DEPTH+1 values -- COUNT versus INDEX -- so this
// cannot wrap silently.
property p_occupancy_bounded;
@(posedge clk) disable iff (!rst_n)
(occupancy <= DEPTH);
endproperty
assert property (p_occupancy_bounded)
else $error("tracker: occupancy %0d exceeds depth", occupancy);
// ---- P9. FORBIDDEN. Occupancy never underflows. A release from an
// empty tracker means a response without a request.
property p_no_release_when_empty;
@(posedge clk) disable iff (!rst_n)
(occupancy == 0) |-> !resp_valid;
endproperty
assert property (p_no_release_when_empty)
else $error("tracker: response released from an empty tracker");
// ---- P10. PARTITION. Released plus outstanding equals allocated.
// Nothing is lost and nothing is duplicated -- the accounting
// property that replaces the liveness assertion this file omits.
property p_no_loss_no_duplication;
@(posedge clk) disable iff (!rst_n)
(released + occupancy == allocated)
|| (allocated == 16'hFFFF) || (released == 16'hFFFF);
endproperty
assert property (p_no_loss_no_duplication)
else $error("tracker: %0d released + %0d outstanding != %0d allocated",
released, occupancy, allocated);
// ---- P11. FORBIDDEN. A completion with no matching outstanding
// entry is counted, never silently dropped. §11: it means a
// response exists for a request nobody allocated.
property p_unmatched_done_counted;
@(posedge clk) disable iff (!rst_n)
(unmatched_done != 16'd0) |=> (unmatched_done >= $past(unmatched_done));
endproperty
assert property (p_unmatched_done_counted)
else $error("tracker: unmatched completion count decreased");
// ---- P12. FORBIDDEN. Completions never exceed allocations. More
// completions than allocations means the data path is inventing
// responses.
property p_completed_le_allocated;
@(posedge clk) disable iff (!rst_n)
(completed <= allocated) || (completed == 16'hFFFF);
endproperty
assert property (p_completed_le_allocated)
else $error("tracker: %0d completions against %0d allocations", completed, allocated);
// ---- P13. A released response's ID is one that was allocated. §6:
// releasing an ID nobody requested breaks the response contract.
property p_resp_id_was_allocated;
@(posedge clk) disable iff (!rst_n)
(resp_valid && resp_ready) |-> (allocated != 16'd0);
endproperty
assert property (p_resp_id_was_allocated)
else $error("tracker: released a response with nothing ever allocated");
// ---- antecedent covers. Without these a silent pass is
// indistinguishable from an unbound checker.
cover property (@(posedge clk) disable iff (!rst_n) req_valid && req_ready);
cover property (@(posedge clk) disable iff (!rst_n) part_valid && part_ready);
cover property (@(posedge clk) disable iff (!rst_n) part_valid && !part_last);
cover property (@(posedge clk) disable iff (!rst_n) part_valid && part_first && part_last);
cover property (@(posedge clk) disable iff (!rst_n) splits_occurred != 16'd0);
cover property (@(posedge clk) disable iff (!rst_n) alloc_valid && alloc_ready);
cover property (@(posedge clk) disable iff (!rst_n) full);
cover property (@(posedge clk) disable iff (!rst_n) done_valid);
cover property (@(posedge clk) disable iff (!rst_n) resp_valid && resp_ready);
cover property (@(posedge clk) disable iff (!rst_n) occupancy == 0);
cover property (@(posedge clk) disable iff (!rst_n) occupancy == DEPTH);
cover property (@(posedge clk) disable iff (!rst_n) unmatched_done != 16'd0);
cover property (@(posedge clk) disable iff (!rst_n) alloc_valid && resp_valid && resp_ready);
endmodule13. DV — Testing the Transformation
// SIMULATION-ONLY. Independent reference. It computes the expected
// split by ENUMERATING boundary crossings from the address arithmetic,
// rather than by running the DUT's incremental subtraction -- a
// different formulation, so agreement is evidence.
class split_reference #(parameter int BOUND_LOG = 10);
localparam longint BOUND = longint'(1) << BOUND_LOG;
// How many parts a request at `addr` of `bytes` becomes. Derived
// from the boundary arithmetic directly: the number of boundaries
// strictly inside the request's byte range, plus one.
function int expected_parts(longint addr, longint bytes);
longint first_b, last_b;
if (bytes <= 0) return 0;
first_b = addr / BOUND;
last_b = (addr + bytes - 1) / BOUND;
return int'(last_b - first_b) + 1;
endfunction
// The byte count of part `k`, computed independently of the DUT.
function longint part_bytes(longint addr, longint bytes, int k);
longint cur = addr, left = bytes, take;
for (int i = 0; i <= k; i++) begin
take = BOUND - (cur % BOUND);
if (take > left) take = left;
if (i == k) return take;
cur += take;
left -= take;
end
return 0;
endfunction
// §4's identity, independently: total parts across a stream equals
// transactions plus splits.
function int splits_for(longint addr, longint bytes);
int p = expected_parts(addr, bytes);
return (p > 0) ? p - 1 : 0;
endfunction
endclass
// SIMULATION-ONLY. Independent reference for §11's ordering contract.
// It models release order with an ASSOCIATIVE ARRAY of per-ID queues
// rather than an age-ordered array -- a different data structure, so
// agreement about ordering is evidence rather than a restatement.
class order_reference;
int alloc_seq[$]; // allocation order, by sequence number
int id_of[int]; // seq -> bus id
bit done_of[int]; // seq -> complete?
function void alloc(int seq, int id);
alloc_seq.push_back(seq);
id_of[seq] = id;
done_of[seq] = 0;
endfunction
function void complete(int seq);
if (done_of.exists(seq)) done_of[seq] = 1;
endfunction
// Head-only release, modelled from the allocation list. Returns -1
// when nothing is releasable, so "not yet" is distinct from "seq 0".
function int releasable();
if (alloc_seq.size() == 0) return -1;
if (!done_of[alloc_seq[0]]) return -1;
return alloc_seq[0];
endfunction
function void release_head();
if (alloc_seq.size() > 0) void'(alloc_seq.pop_front());
endfunction
// The contract that matters: for any two outstanding entries with
// the SAME id, the earlier-allocated one must release first.
function bit same_id_order_preserved(int released_order[$]);
foreach (released_order[i])
for (int j = i + 1; j < released_order.size(); j++)
if (id_of[released_order[i]] == id_of[released_order[j]]
&& released_order[i] > released_order[j]) return 0;
return 1;
endfunction
endclass| Check | What it establishes |
|---|---|
| 20,000 random (address, length) pairs through splitter and reference | Incremental subtraction and boundary enumeration agree on part count |
| Every part's byte count compared | Per-part arithmetic agrees, including the final short part |
| Request exactly filling one boundary region | expected_parts = 1; no split |
| Request starting one byte before a boundary | expected_parts = 2; first part is 1 byte |
| Request spanning three boundaries | expected_parts = 4 |
| Zero-byte request | Reference returns 0; DUT never asserts part_valid |
Assert req_valid mid-split | req_ready low — P1 |
Check parts = txns + splits over a long stream | P5 holds exactly |
Allocate DEPTH then one more | alloc_ready low — P7 |
| Complete out of order, two different IDs | Both release; head-only order observed |
| Complete out of order, two same IDs | Earlier allocation releases first — the reference agrees |
| Complete a tag never allocated | unmatched_done rises — P11 |
| Release with empty tracker | P9 fires |
| Allocate and release in the same cycle | Occupancy correct; tail index accounts for the shift |
Random stalls on part_ready and resp_ready | Accounting holds; P10 partition never breaks |
| Run with the requester disconnected | All 13 properties pass; all 13 covers empty |
The last row is the report worth publishing, and it is the one an SoC integration most often ships:
A BOUND CHECKER THAT PROVED NOTHING
stimulus : both blocks instantiated and reset; no requester attached
p_no_accept_mid_split PASS
p_bus_id_preserved PASS
p_part_fits PASS
p_split_accounting PASS
p_no_alloc_when_full PASS
p_no_release_when_empty PASS
p_no_loss_no_duplication PASS
... all 13 properties PASS
------------------------------------------------
cover req_valid && req_ready 0 hits
cover splits_occurred != 0 0 hits
cover occupancy == DEPTH 0 hits
... all 13 covers 0 hits
eight of the thirteen properties are implications and never armed.
The five INVARIANTS -- occupancy bound, the partition, completions
bounded, split accounting, unmatched monotone -- all pass on zeros:
0 + 0 == 0.
diagnosis : an integration report reading "AXI-to-controller
boundary: all assertions pass" is produced by this run AND by a
fully exercised one. The covers are the only thing that separates
them, and the split cover is the specific one that matters --
a boundary adapter whose splits_occurred cover is empty has
never been given a crossing request, which is the case its whole
existence is about.
the fix : read the covers. Specifically, require the
splits_occurred and occupancy == DEPTH covers before accepting
any statement about the boundary.14. Where Latency Actually Comes From
DDR DEVICE LATENCY IS NOT SoC MEMORY-ACCESS LATENCY, and this is the module's second law.
Chapter 23.1 owns the decomposition of a request's end-to-end latency into named components that sum exactly to the total — within the DDR domain. This chapter's contribution is that the DDR domain is not the whole path, and the components above it are frequently larger.
| Component | Owner | Present in every SoC? |
|---|---|---|
| Requester issue and buffering | 29.3 | yes, varies |
| Cache lookup and miss handling | CHI 1.4 | only where caches exist |
| Address translation | §16 | architecture-dependent |
| Coherency resolution | CHI 2.8 | only in coherent paths |
| Fabric traversal and arbitration | AXI 12.4 | yes, depth varies |
| Controller queueing | 17.2 | yes |
| Scheduling delay | 17.4, 23.4 | yes |
| Row-state effects | Module 9 | yes |
| DDR timing | Module 14, 23.1 | yes |
| PHY transfer | Module 19 | yes |
| Return path | the fabric again | yes |
So reducing CPU-visible memory latency to CAS latency is wrong by most of the path. Chapter 14.9 owns CL; it is one component of one row of this table.
15. Where Things Break at This Boundary
Six failure modes specific to the transformation, using Module 28's reasoning rather than repeating it.
| Symptom | Where it first becomes visible | Discriminating observation |
|---|---|---|
| Latency far above DDR expectation, low DRAM utilisation | queue occupancy high, data bus idle | §2 — the queue measures arrival vs service, not DRAM busy |
| One requester slow, others fine | arbitration counters per source | 29.2 §6 — a QoS or fairness effect |
| Responses out of order for one ID | the tracker's release order | §6 — the front end, not the scheduler |
| A response for a request nobody made | unmatched_done | §11 — accounting loss upstream |
| Bandwidth far below the bus's capability | row-hit rate at the controller | §5 — burst length helped the bus, not the rows |
| Sporadic latency spikes at a fixed period | refresh drain — 28.3 §6 | that chapter's window-position histogram |
| A full queue blamed on DRAM | data-bus utilisation alongside occupancy | §2 — both must be read together |
Row one is the most common misdiagnosis in SoC memory work. A full request queue looks like a busy DRAM and is frequently reported as one. It can equally mean the scheduler is blocked on a timing constraint, or that one bank is in tRC while every queued request targets it. The discriminating measurement is data-bus utilisation, and 23.2 owns it.
16. Virtual, Physical, and DDR Addresses
Three address spaces, and conflating any two produces a diagram that cannot be right.
A requester may issue a virtual address. Whether it does is architecture-dependent: a CPU core does, a DMA engine may or may not, an accelerator may or may not.
virtual address
| translation, WHERE PRESENT -- architecture-dependent
v
physical address
| SoC memory-region decode (AXI 12.3)
v
controller / channel selection
| DDR address mapping (8.6, 18.1)
v
rank / bank group / bank / row / columnTwo things this chapter needs from it, and no more.
A virtual address must never be drawn entering a DDR bank mapper. The mapper consumes a physical address, and the bits it slices are physical bits. A diagram showing otherwise implies the mapping is stable under a page remap, which it is not — the same virtual address can map to different physical pages and therefore different banks and rows across runs.
And a translation can itself generate memory traffic. A page-table walk is a memory access, so a translation miss produces additional DDR requests that the original request implicitly caused. That is architecturally important and easy to miss: a workload with poor translation locality generates a DDR traffic stream nobody wrote into the algorithm. Chapter 29.3 treats it as one of the traffic sources a CPU produces.
What this chapter does not do is teach translation. Where the repository owns a mechanism it is linked; where it does not, the architectural statement is that the layer may or may not be present, and its presence is a property of the SoC rather than of DDR.
17. Performance Implications
Three consequences of the transformation that matter more than any number.
The expansion factor is a locality result. §3 showed one request costing two to four commands depending on row state. Across a stream, the average expansion factor is essentially the row-hit rate, which 23.3 owns — and that rate is determined by the address stream's shape and the mapping policy, not by the controller alone.
More outstanding requests do not always help. Chapter 8.1 owns why outstanding exists and 13.3 owns depth tuning. From the DDR side there are two distinct regimes:
| Regime | More outstanding |
|---|---|
| The controller has too few candidates to find row hits | helps — it gives the scheduler choices, which is 23.4's raw material |
| The controller already has enough candidates | does not help throughput, and raises queueing delay for everyone |
| The reorder tracker is the limit — §11 | cannot help, because the front end will not release faster |
The third row is the one that surprises people, and it is this chapter's own RTL that shows why: §11's tracker depth bounds how much reordering can be exploited. Adding bus-side outstanding capacity beyond that depth adds queueing delay and no throughput.
And scheduler reordering cannot repair arbitrary locality destruction. Chapter 23.4 reorders within its queue window. If the address stream has no row locality within that window, there is nothing to find — which is the architectural insight 29.5 §8 develops: architecture creates opportunities; scheduling exploits the opportunities that remain.
18. Misconceptions
“An AXI read maps to one DDR READ.” §3, §5. It maps to one to three commands depending on row state, and to one or more DDR bursts depending on the interface width and device burst length.
“AXI burst length equals DDR burst length.” §5. They are independent parameters — one per-transaction and chosen by the requester, one a device property owned by 12.1.
“AXI acceptance means memory service has begun.” §2. At the handshake the address has not been mapped, the row state is unknown, and no command has been committed.
“A full request queue means DRAM is busy.” §2, §15. It means arrivals outran service. The data-bus utilisation is the measurement that distinguishes them.
“The controller sees my transaction.” §1. It sees a queue entry that may be one of several parts, carrying an internal tag and a preserved bus ID.
“Longer bursts are better for DRAM.” §5. Longer bus bursts amortise bus overhead — 19.2 owns that. For DRAM the benefit is indirect: more consecutive columns in one row, which is a locality benefit.
“The scheduler honours my transaction order.” §6. It reorders for row locality and knows nothing about bus IDs. The front end enforces the ordering rule, at the cost of storage.
“More outstanding requests always improve performance.” §17. Beyond the point where the scheduler has enough candidates, and beyond the reorder tracker's depth, extra outstanding adds delay and no throughput.
“CAS latency is the memory latency my CPU sees.” §14. It is one component of one row in a table of eleven, several of which are larger.
“A physical address has one universal DDR mapping.” §16, and 18.1 owns the policy space. The mapping is an implementation choice, and the same address lands in different banks under different policies.
“I can send a virtual address to the bank mapper.” §16. The mapper slices physical bits, and a page remap changes which bank a virtual address reaches.
19. Interview Reasoning
Walk one AXI read to the DRAM pins. Acceptance and queue allocation; address mapping to rank, bank group, bank, row and column; row-state classification giving one to three commands; timing legality; commit; data beats; reassembly and an ordering-correct response.
What has happened at the instant ARVALID and ARREADY are both high? The metadata is captured and a queue entry exists. Nothing has been mapped, no row state consulted, no command committed.
How many DDR commands does a 64-byte read cost? Between two and four in the worked example — one to three row-management commands plus as many read bursts as the width and device burst length require. It depends on state the requester cannot see.
Why can a legal aligned burst still split? Because the split boundary is a property of the address, not the request. A row or channel boundary can fall inside a perfectly formed burst.
Who enforces same-ID ordering, the scheduler or the front end? The front end. The scheduler reorders for row locality and does not know about bus IDs.
What does that enforcement cost? Storage. A reorder tracker's depth bounds how much scheduler reordering can be exploited, which makes it an architectural dial rather than a tuning knob.
Your request queue is full. Is DRAM saturated? Not necessarily. Read the data-bus utilisation: the queue can be full while the bus idles because every queued request targets a bank in a timing window.
Would doubling outstanding depth help? Only if the scheduler currently lacks candidates to find row hits. If it has enough, or if the reorder tracker is the limit, you add queueing delay and no throughput.
Why is CAS latency not the CPU's memory latency? Because the path also includes cache miss handling, possible translation, possible coherency, fabric traversal and arbitration, queueing, scheduling, row effects, PHY transfer and the return path.
Why not just add those up? They are jointly determined, they overlap under outstanding requests, and which of them exist depends on the SoC. Causal decomposition on a specific system replaces a universal equation.
20. Exercises
-
§4 lists four split causes. For each, say whether the requester could have avoided it, and what information it would have needed.
-
§9's worked trace gives two DDR bursts for a 64-byte request. Recompute for a 64-bit interface and for device burst length 16, and state which factor dominates.
-
§6 puts same-ID ordering in tension with row-locality reordering. Derive the reorder-tracker depth needed to allow a scheduler window of
Wrequests without stalling, stating your assumption about ID distribution. -
§11's tracker releases from the head only. Construct the traffic pattern where that costs the most throughput, and estimate the cost as a fraction.
-
§14 argues against a latency equation. Choose two components from its table and construct the workload where their sum overstates the real latency by the largest factor.
-
§16 notes that a translation miss generates extra DDR traffic. Bound that traffic for one original request, stating your assumption about translation-structure depth.
-
§17 gives three outstanding-depth regimes. Design the measurement that tells you which regime a running system is in, using only controller-side counters.
-
A colleague proposes that the requester specify a target bank to improve row locality. Identify every architectural reason this cannot work, using §7's diagram.
21. Where This Goes
An SoC request is not a DDR command. Its count changes at conversion and mapping — one transaction becoming several requests and each request one to three commands; its identity must be preserved across a boundary where both sides reorder for different reasons; its order is constrained by one rule above the boundary and a different rule below it; and acceptance establishes responsibility, not service.
Four results carry forward. The expansion factor is a property of the stream, not the request — the same access costs two to four commands depending on what earlier traffic left in that bank. The ordering boundary costs storage, and that storage bounds how much scheduler reordering is exploitable. A full queue is not a busy DRAM, and data-bus utilisation is the measurement that separates them. And there is no latency equation — the components are jointly determined, they overlap, and which exist depends on the SoC.
Two things stay open. The split boundary is a parameter, so §12's properties are all satisfied by a boundary that does not match the real row or channel granularity. And head-only release is stricter than the protocol requires, which §11 states rather than hides, because teaching the simplification silently would teach a false performance model.
Chapter 29.2 takes the controller itself. This chapter treated it as the far side of a boundary; the next asks where it sits in the SoC and what contracts exist on each of its sides — because the controller is not really a DDR block with a bus port. It is a semantic adapter with SoC transactions on one side and DRAM resource constraints on the other, and the two sides disagree about almost everything: about what a request is, about what ordering means, about what fairness means, and about whether a requester's priority should be allowed to matter at all.
Continue learning
Related tutorials
- Related topic
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.
- Related topic
Memory Controllers in SoCs
A semantic adapter between two sides that disagree about what a request is, what order means, what fairness means, and whether priority should matter at all.
- Related topic
CPU Access Patterns
Most loads never reach DRAM. What does is shaped less by how many loads a program executes than by its dependency structure — and that decides whether latency or bandwidth matters.
- Related topic
DMA Access Patterns
A DMA transfer is not one stream. It is a payload stream and a descriptor stream with opposite properties, and the second is the one nobody counts.
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.
