Skip to content
VLSI Mentor

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.

ObjectLives onIdentified byOwned by
Bus transactionthe interconnecta bus ID plus its own handshake5.1, 8.3
Controller requestthe controller's queuean internal tag17.2
DDR commandthe DRAM interfacenothing — it is an eventModule 7
Data beatthe data busits position in a burst12.1

None of the four maps one-to-one onto any other, and treating any pair as equal produces a specific wrong prediction:

Assumed equalityWhat it predictsWhy it is wrong
bus transaction = controller requestone AXI read → one queue entry§4 — boundary splitting
controller request = DDR commandone queue entry → one RD§3 — a request may need PRE + ACT + RD
bus burst = DDR burstAXI length = DDR burst length§5 — independent parameters
acceptance = serviceARREADY 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.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   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 it

So 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 scheduledCommands requiredOwner
The target row is already openRD alone9.3
The bank is idleACT then RD9.4
A different row is open in that bankPRE, ACT, then RD9.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.

CauseMechanismOwner
A burst crosses a controller or channel boundarythe decode routes halves to different destinations12.3
A burst is longer than the controller's request granularitythe front end splits to its internal size17.5
Data-width conversiona wide beat becomes several narrow ones, or the reverse7.7
A burst crosses a DRAM row boundarythe second part needs a different row — 8.6Module 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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   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 burstDDR burst
Set bythe requester, per transactionthe device generation and mode — 12.1
Unitsbeats of the bus data widthtransfers on the DQ bus
Varies per transactionyesno — it is a device property
Owner7.112.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 rule23.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.

ConstraintSourceWho must enforce it
Same-ID responses in order8.3the controller front end
Different-ID responses may complete out of order8.4nobody — it is permission
Split parts of one transaction recombine in orderthe transaction's own semanticsthe front end
Row-locality reordering23.4the 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

The path from a requester to DRAM, in four rows, showing where each transformation happens. The top row holds three requester classes — a CPU cluster whose accesses are driven by cache misses, a DMA engine whose accesses are programmed transfers, and an accelerator whose accesses are a streaming consequence of on-chip reuse — together with the address decode that routes by physical address. The second row holds the interconnect layer: the coherent or non-coherent fabric that carries the transaction, the arbitration that decides which requester proceeds, and the width and boundary conversion that can split one transaction into several. The third row holds the memory controller's front end, its request queue, the address mapper that turns a physical address into a rank, bank group, bank, row and column, and the scheduler that selects a legal command. The bottom row is the physical layer and the DRAM device itself. Edges run from all three requester classes into the fabric, from the address decode into the fabric, from the fabric through arbitration and conversion into the controller front end, from the front end into the queue, from the queue into both the address mapper and the scheduler, from the mapper into the scheduler, from the scheduler into the physical layer, and from the physical layer into the DRAM device. The important structural point is that no edge runs directly from any requester to the DRAM device: every path passes through the mapper and the scheduler, which is where a request's count, identity and order can all change.CPU clustercache misses — 29.3DMA engineprogrammed transfers —29.4Acceleratorreuse residue — 29.5Address decoderoutes by physical addr —12.3Fabriccoherent or not — CHI 3.7Arbitrationwhich requester — AXI12.4Width / boundaryone txn may split — §4Controller front end17.5's contractRequest queue17.2 — allocationAddress mapper8.6 — bank/row/columnScheduler17.1 — the commit pointPHY and DRAMModule 19, the device12

8. One Request, End to End

A sequence diagram with six participants tracing one cache-line-sized read from bus acceptance to response. The requester issues a read address transaction, and the controller front end accepts it with a handshake, which transfers responsibility but begins no DRAM activity. The front end allocates a queue entry holding the address, length, size and identifier, and the identifier is retained because responses for the same identifier must return in order. The queue entry is passed to the address mapper, which returns the rank, bank group, bank, row and column that the physical address decomposes to. The scheduler then classifies the row state for that bank, and the answer determines the command count: a row hit needs a read alone, an idle bank needs an activate then a read, and a different open row needs a precharge, an activate and a read. The scheduler waits for timing legality before committing, which is the only instant architectural state changes. It issues the commands to the device, the device returns data beats after the read latency, and the front end reassembles the beats and returns a response to the requester in an order consistent with the identifier's ordering rule. The dashed return arrows mark the response path. The important point the diagram makes is that the acceptance handshake and the first command are separated by four steps, any of which can reorder this request behind others accepted after it.A cache-line read, from bus acceptance to responseRequesterFront endQueueMapperSchedulerDRAMread addr + len +size + IDaccepted — notservedallocate entry,retain IDphysical addressrank/bg/bank/row/colcandidate — not acommandrow state: 1, 2 or 3commandscommit: PRE / ACT /RDdata beatsresponse, ID orderhonoured

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.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   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 request

The 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

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ---------------------------------------------------------------------
// 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
endmodule

req_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

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ---------------------------------------------------------------------
// 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
endmodule

Head-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

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// 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);
endmodule

13. DV — Testing the Transformation

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// 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
CheckWhat it establishes
20,000 random (address, length) pairs through splitter and referenceIncremental subtraction and boundary enumeration agree on part count
Every part's byte count comparedPer-part arithmetic agrees, including the final short part
Request exactly filling one boundary regionexpected_parts = 1; no split
Request starting one byte before a boundaryexpected_parts = 2; first part is 1 byte
Request spanning three boundariesexpected_parts = 4
Zero-byte requestReference returns 0; DUT never asserts part_valid
Assert req_valid mid-splitreq_ready low — P1
Check parts = txns + splits over a long streamP5 holds exactly
Allocate DEPTH then one morealloc_ready low — P7
Complete out of order, two different IDsBoth release; head-only order observed
Complete out of order, two same IDsEarlier allocation releases first — the reference agrees
Complete a tag never allocatedunmatched_done rises — P11
Release with empty trackerP9 fires
Allocate and release in the same cycleOccupancy correct; tail index accounts for the shift
Random stalls on part_ready and resp_readyAccounting holds; P10 partition never breaks
Run with the requester disconnectedAll 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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  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 totalwithin 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.

ComponentOwnerPresent in every SoC?
Requester issue and buffering29.3yes, varies
Cache lookup and miss handlingCHI 1.4only where caches exist
Address translation§16architecture-dependent
Coherency resolutionCHI 2.8only in coherent paths
Fabric traversal and arbitrationAXI 12.4yes, depth varies
Controller queueing17.2yes
Scheduling delay17.4, 23.4yes
Row-state effectsModule 9yes
DDR timingModule 14, 23.1yes
PHY transferModule 19yes
Return paththe fabric againyes

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.

SymptomWhere it first becomes visibleDiscriminating observation
Latency far above DDR expectation, low DRAM utilisationqueue occupancy high, data bus idle§2 — the queue measures arrival vs service, not DRAM busy
One requester slow, others finearbitration counters per source29.2 §6 — a QoS or fairness effect
Responses out of order for one IDthe tracker's release order§6 — the front end, not the scheduler
A response for a request nobody madeunmatched_done§11 — accounting loss upstream
Bandwidth far below the bus's capabilityrow-hit rate at the controller§5 — burst length helped the bus, not the rows
Sporadic latency spikes at a fixed periodrefresh drain — 28.3 §6that chapter's window-position histogram
A full queue blamed on DRAMdata-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.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   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 / column

Two 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:

RegimeMore outstanding
The controller has too few candidates to find row hitshelps — it gives the scheduler choices, which is 23.4's raw material
The controller already has enough candidatesdoes not help throughput, and raises queueing delay for everyone
The reorder tracker is the limit — §11cannot 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

  1. §4 lists four split causes. For each, say whether the requester could have avoided it, and what information it would have needed.

  2. §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.

  3. §6 puts same-ID ordering in tension with row-locality reordering. Derive the reorder-tracker depth needed to allow a scheduler window of W requests without stalling, stating your assumption about ID distribution.

  4. §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.

  5. §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.

  6. §16 notes that a translation miss generates extra DDR traffic. Bound that traffic for one original request, stating your assumption about translation-structure depth.

  7. §17 gives three outstanding-depth regimes. Design the measurement that tells you which regime a running system is in, using only controller-side counters.

  8. 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

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.