Skip to content
VLSI Mentor

DDR · Module 11

The Write Command

Accepting a read creates an obligation to recognise something. Accepting a write creates an obligation to produce something — and that single reversal explains almost every way writes differ from reads.

Module 10 built a read as an executable transaction: created at acceptance, predicted across an interval, delivered as beats through a boundary, completed on evidence.

This module builds the other direction, and it is not the same transaction reversed.

A read's controller waits. A write's controller owes.

That is the whole of the asymmetry, and this chapter's question follows directly from it:

What does accepting a write actually start?

The short answer is an obligation to deliver — and an obligation to deliver is a fundamentally different object from an obligation to recognise. It has a deadline the controller must meet rather than a window it must watch, it carries data rather than waiting for it, and it does not end when the data is gone.

1. Where This Chapter Starts

The precondition is identical to a read's, and worth restating because it is the one thing writes and reads genuinely share.

A write command is accepted when the bank it names already holds the row it wants. Chapter 9.3's row hit: same bank, same row, and a bank in BANK_OPEN.

A write does not open a row. Chapter 9.1 established that only an activate does, and Chapter 8.2 established that a column command carries no row address. A write arriving at a closed bank is not a slow write — its precondition is absent.

So the state of the world at cycle zero:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   bank B is OPEN, holding row R
   the requester wants to write column C of that row
   the requester has the data
   the classification is REQ_HIT
   nothing is on the data bus
   the DRAM does not own the data bus — and neither, yet, does the controller

That last line is where reads and writes part company.

2. The Asymmetry That Shapes the Module

Set the two directions side by side, because almost every difference in this module is a consequence of one row of this table.

READWRITE
Command directioncontroller → DRAMcontroller → DRAM
Data directionDRAM → controllercontroller → DRAM
DQ sourceDRAMcontroller / PHY
DQS sourceDRAMcontroller / PHY
Controller's job after the commandwatch for dataproduce data
If the controller is latedata is misseddata is never delivered
Transaction ends atthe final beat receivednot the final beat sent

The command rows are identical. Every data row is reversed.

And the last row of the table is the module's other half. A read is finished when its last beat has been received: the data is in the controller, and nothing further is owed by anyone. A write is not finished when its last beat has been sent. The device still has internal work to do before the row can safely be closed, and that obligation is Chapter 11.4's — the single largest structural difference between the two directions.

3. Three Different Events Called "Accepted"

This module will use the word accepted precisely, because there are three distinct events and conflating them makes the rest incomprehensible.

EventWho acceptsWhat it meansOwned by
Request admittedthe controllerthe controller has taken responsibility for this writethis chapter
Command acceptedthe DRAMa well-formed WRITE was sampled against a legal stateChapter 7.3
Data consumedthe PHY / devicewrite beats have actually crossed the interfaceChapter 11.3

These happen at three different times, in that order, and each can occur without the next.

A request can be admitted and its command never issued — because timing did not permit it yet, which is Modules 13 and 14'. A command can be accepted and its data never delivered — which is §9's debugging case and is the write-specific failure with no read analogue. And data can be delivered while the write is still not finished, which is Chapter 11.4.

4. What Acceptance Creates — an Obligation to Deliver

Chapter 10.1 §3 listed what a read's transaction carries: identity, metadata, expectation. A write's carries all three and one more, and the extra one changes its character entirely.

Identity. A tag, so that a later launch can be attributed to this request.

Metadata. Bank and column, plus the open-row context captured at acceptance — for exactly the reason Chapter 10.1 §8 gave: the row lives in bank state, and that state can change before the write is delivered.

Expectation. When the data must be launched, which is Chapter 11.2's, and how much of it, which is Chapter 11.3's.

And a payload the controller still owes. This is the new thing. The data exists — the requester has it — and it has not gone anywhere. Until it is delivered, the controller is holding something it is obliged to put on a bus at a specific time.

That obligation has three properties a read's has none of:

It can be defaulted on. A read that is not watched for loses data that already exists in the device. A write that is not delivered leaves the device expecting data that never comes, and what the device writes instead is not defined by anything the controller chose.

It occupies resources. The payload must be held somewhere until launch. A read needs a record; a write needs a record and the data, and at burst widths that is not a trivial quantity.

It is not discharged by the command. Issuing the WRITE does not deliver anything. The command and the data are separate events on separate wires at separate times, which is Chapter 11.2's subject and the reason a write latency exists at all.

A sequence from the requester through the controller to the PHY and the DRAM. The requester presents a write request naming a bank and a column and carrying data. The controller checks the row-state classification and admits the request, creating a pending transaction that records identity, metadata and an owed payload. The controller issues the write command to the DRAM. Later, at a configured launch relationship, the controller hands the payload to the PHY, which drives it toward the DRAM as a burst. The DRAM receives the burst. After the final beat the DRAM still has internal work before the row may be closed, so the transaction is not yet finished.Admission to delivery, and beyondRequesterControllerPHY boundaryDRAMwrite B/C + dataadmitted — payloadowedWRITE issuedpayload, laterburst drivenstill not finished
Figure 1 — A write transaction, and the payload the controller owes across it.

Read the third and fourth arrows. The command goes out; the payload goes out later, on different wires. Between them the controller is holding data with a deadline — and the last arrow is Chapter 11.4's warning that even delivering it does not end the transaction.

5. RTL — Admitting a Write and Owing a Payload

The engineering problem

Accept a write request only when its row state permits, and create exactly one transaction record that carries identity, metadata, and an explicit record that a payload is still owed — so that an undelivered write is a visible state rather than an absence.

Why hardware needs it

Without a record, a launch cannot be attributed and an undelivered payload cannot be detected. The read side needed a record to recognise a return; the write side needs one to remember a debt.

Classification

SYNTHESIZABLE EDUCATIONAL CONTROLLER RTL.

What it models

Admission of a write against a supplied row-state classification; creation, retention and retirement of one pending-write record; the owed-payload state; and rejection with a distinguishable reason.

What it does NOT model

Classification (Chapter 9.3, consumed). Row state (Chapter 9.1). Command encoding (Chapter 7.1). Launch timing (Chapter 11.2) — nothing here knows when the data is due out. Beats or the bus (Chapter 11.3). Recovery (Chapter 11.4). Masking (Chapter 6.11). Scheduling (Module 17). Timing legality (Modules 13 and 14) — admission means the state permits the command, never that it may be issued this cycle.

Interface and parameter contract

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ─────────────────────────────────────────────────────────────────────────
// write_request_admit
//
// Classification: SYNTHESIZABLE EDUCATIONAL CONTROLLER RTL.
//
// MODELS: admission of a write against a supplied row-state
// classification, and creation/retention/retirement of ONE pending-write
// transaction -- including the OWED-PAYLOAD state that distinguishes a
// write's obligation from a read's.
//
// ONE OUTSTANDING WRITE. A second request while one is pending is REJECTED
// with a reason, never queued (Module 17 owns scheduling). Chapter 11.5
// revisits multiple outstanding writes.
//
// RELATION TO EXISTING BLOCKS: Chapter 7.3's cmd_accept_pipeline watches
// DECODED COMMANDS ARRIVING AT THE DEVICE and decomposes them into
// observed/accepted/completed against bank state. THIS block sits one step
// earlier, on the REQUESTER's side, and produces a transaction with a tag,
// a column and an owed payload -- none of which that pipeline has. They
// are different events (Chapter 11.1 Section 3), not the same one twice.
//
// RECORDS THAT A PAYLOAD IS OWED; DOES NOT STORE THE PAYLOAD. Where write
// data is buffered is a microarchitectural decision this block does not
// make. Chapter 11.3 takes the data as an input at launch time.
//
// DOES NOT MODEL: launch timing (Chapter 11.2), beats or the bus (Chapter
// 11.3), recovery (Chapter 11.4), masking (Chapter 6.11), scheduling
// (Module 17), or timing legality (Modules 13, 14).
//
// MODELS NO PHYSICAL OR ANALOG BEHAVIOUR.
// ─────────────────────────────────────────────────────────────────────────
module write_request_admit #(
  parameter int NUM_BANKS = 4,
  parameter int COL_W     = 10,
  parameter int TAG_W     = 3,
  parameter int BA_W      = (NUM_BANKS <= 1) ? 1 : $clog2(NUM_BANKS)
) (
  input  logic                 clk,
  input  logic                 rst_n,

  // ── Request from above. Bank and column already derived by Module 8's
  //    address map. The DATA itself stays with the requester until launch.
  input  logic                 req_valid,
  input  logic [BA_W-1:0]      req_bank,
  input  logic [COL_W-1:0]     req_col,

  // ── Row-state classification from Chapter 9.3. Read, never recomputed.
  input  req_class_e           req_class,

  output logic                 req_accept,

  // ── Write-command intent, for Chapter 7.1's encoder. NOT an encoding.
  output logic                 wr_cmd_valid,
  output logic [BA_W-1:0]      wr_cmd_bank,
  output logic [COL_W-1:0]     wr_cmd_col,

  // ── The transaction record.
  output logic                 pending,
  output logic [TAG_W-1:0]     pending_tag,
  output logic [BA_W-1:0]      pending_bank,
  output logic [COL_W-1:0]     pending_col,

  // ── THE WRITE-SPECIFIC STATE. High from admission until the burst has
  //    been delivered. A read's record waits to RECEIVE; this one owes a
  //    DELIVERY, and an undelivered write is a visible condition rather
  //    than merely the absence of something.
  output logic                 payload_owed,

  // ── Delivery finished, from Chapter 11.3's sequencer. NOTE THE NAME: the
  //    burst left the controller. It does NOT mean the write is finished --
  //    Chapter 11.4 is about what is still owed after this.
  input  logic                 burst_done,

  // ── Retirement, from Chapter 11.5's tracker, once every obligation
  //    including recovery has been discharged.
  input  logic                 retire,

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

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

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

  logic is_hit;
  assign is_hit = (req_class == REQ_HIT) && !idx_bad;

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

  // ── Command intent in the acceptance cycle. Combinational deliberately:
  //    a registered intent would be an intent for a row state that may
  //    have moved -- Chapter 10.1 Section 12's exercise 4.
  assign wr_cmd_valid = req_accept;
  assign wr_cmd_bank  = req_bank;
  assign wr_cmd_col   = req_col;

  logic [TAG_W-1:0] tag_ctr_q;

  always_ff @(posedge clk) begin
    if (!rst_n) begin
      pending      <= 1'b0;
      pending_tag  <= '0;
      pending_bank <= '0;
      pending_col  <= '0;
      payload_owed <= 1'b0;
      // Tags restart at zero. A monitor spanning a reset must therefore
      // flush rather than keep matching against reused identifiers.
      tag_ctr_q    <= '0;
    end else begin
      if (req_accept) begin
        pending      <= 1'b1;
        pending_tag  <= tag_ctr_q;
        pending_bank <= req_bank;
        pending_col  <= req_col;
        // The debt is incurred at admission, not at command issue.
        payload_owed <= 1'b1;
        tag_ctr_q    <= tag_ctr_q + 1'b1;
      end else begin
        // The debt is discharged by DELIVERY, which is a different event
        // from retirement. Between them the transaction is still pending
        // and no longer owes data -- that gap is Chapter 11.4's subject.
        if (burst_done) payload_owed <= 1'b0;
        if (retire) begin
          pending <= 1'b0;
          // Metadata is NOT cleared: it is the last record of what was
          // asked for, which Section 9's debugging uses.
        end
      end
    end
  end

endmodule

State representation and transitions

A pending flag, a tag, the metadata, and the owed-payload flag. Four states in effect, and the fourth is the one with no read analogue:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   idle
     --req_accept-->   pending, payload OWED
     --burst_done-->   pending, payload delivered   ← no read equivalent
     --retire-->       idle

The middle state is the chapter's contribution. A write that has delivered its data but has not retired is a real, occupied condition: the controller owes nothing further on the bus and the transaction is still not finished. Chapter 11.4 is entirely about what happens in it.

Combinational behaviour

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

Sequential behaviour and reset

Nonblocking only. burst_done and retire are handled in the same else branch and are independent — a design may assert both in one cycle if a write's recovery requirement is zero, and both take effect.

Reset clears the record, the debt, and the tag counter. The debt being cleared is worth noting: the device may still be expecting data for a command already issued. A reset does not un-issue a command, so a controller coming out of reset with an open bank and a device awaiting write data is in a state nothing in this block can repair — §9's mechanism 5, and a genuine reason controllers precharge everything after reset.

Cycle-by-cycle trace

CycleInputreq_acceptpendingpayload_owedReject
0idle000
1req_valid, b1, c40, HIT111
2req_valid, b1, c41, HIT011busy
3idle011
4burst_done010
5idle010
6retire000
7req_valid, b3, c00, MISS000not_hit

Cycle 5 is the state that does not exist on the read side. The data has gone, the transaction has not. pending is high and payload_owed is low, and the thing keeping the transaction alive is an obligation the device holds, not one the controller does.

How to simulate, and expected output

Drive the trace and check all seven outputs. Then:

burst_done with nothing pending must change nothing. A spurious delivery signal must not clear a debt that was never incurred.

retire while payload_owed is still high — a transaction retiring before its data was delivered. This block does not forbid it, deliberately: enforcing ordering between two inputs it does not generate would hide a real defect upstream. Chapter 11.5's tracker is where that ordering is asserted, and §7's P4 reports it here.

All four non-hit classes — miss, conflict and both busy states — must give reject_not_hit, never acceptance.

Reset while payload_owed must clear everything and restart tags at zero.

NUM_BANKS = 3 with req_bank = 3 must reject via the index check.

Expected waveform

§6. The shape to look for is two flags falling at different timespayload_owed first, pending later — with a gap between them that nothing in this chapter explains.

Synthesis implications

A flag, a tag counter, BA_W + COL_W metadata bits, and one more flag — about 19 flops at the defaults. The record is cheap; the payload buffer is not, and keeping them separate is what makes that cost visible rather than buried in a transaction structure.

Corner cases

NUM_BANKS == 1 gives BA_W == 1 through the guard and rejects index 1 correctly. Non-power-of-two NUM_BANKS is the only configuration where the index check fires. TAG_W == 1 gives two alternating tags — useful for testing wrap, and the configuration where a monitor's uniqueness assumption breaks fastest. Simultaneous burst_done and retire is legal and both apply, which is the zero-recovery configuration Chapter 11.4 discusses.

Failure modes and debugging clues

payload_owed stuck high means the launch path never delivered — §9. pending stuck high with payload_owed low means the data went out and nothing retired the transaction, which points at recovery or the tracker rather than the data path. req_accept never asserting with hits present means the index check is firing; compare NUM_BANKS against the real bank count.

Extension ideas

Replace the single record with an array indexed by tag and several writes can be outstanding — at which point the payload buffer must also become per-tag, which is the resource cost Chapter 11.5 discusses. Adding a byte-enable handle alongside the payload handle connects this record to Chapter 6.11's masking without duplicating it.

Limitations

One outstanding write, one rank. It does not hold the data, so it cannot detect a payload that was corrupted between admission and launch. It knows nothing about when the data is due, how much there is, or what is owed after delivery — all three are later chapters, and the record deliberately has no field for any of them yet.

6. Acceptance and the Owed Payload, in Cycles

write_request_admit — a debt incurred and a debt discharged

10 cycles
Ten cycles. A write request for bank one column forty with a row-hit classification is accepted at cycle one, asserting the write command intent for a single cycle and setting both the pending flag and the payload-owed flag with tag zero. A second request at cycle two is rejected as busy. At cycle five a burst-done pulse arrives and the payload-owed flag falls while the pending flag stays high. At cycle eight a retire pulse clears the pending flag. The gap between the two flags falling is the interval in which the controller owes nothing further on the bus while the transaction is still not finished.owes dataowes dataowes nothing — still pendingowes nothing — still pendingadmitted · payload owedadmitted · payload owedsecond write: busysecond write: busydata delivereddata deliveredretiredretiredCKreq_validreq_class--HITHIT--------------req_acceptwr_cmd_validpendingpayload_owedburst_doneretirereject_busyt0t1t2t3t4t5t6t7t8t9
Figure 2 — Two flags, falling at different times, with a gap this chapter cannot yet explain. EDUCATIONAL — NOT TO SCALE, NOT JEDEC TIMING.

wr_cmd_valid is high for one cycle. payload_owed is high for four. pending is high for seven.

Three different durations for one write, and that is the chapter in a picture. The command is an event; the debt is an interval; the transaction is longer than both.

Cycles 2 to 5 are the interval Chapter 11.2 and 11.3 own — the controller owes data it has not yet put on the bus. A read's transaction has no equivalent: a read's controller owes nothing at any point.

Cycles 6 to 8 are the interval Chapter 11.4 owns, and this chapter deliberately cannot explain it. The data has gone. The controller owes nothing further on any wire. And the transaction is still not finished, because something in the device is not ready for what a precharge would do.

EDUCATIONAL — NOT TO SCALE, NOT JEDEC TIMING. Both gaps are drawn for readability and correspond to no device.

7. Four Assertions Worth Writing

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

// P2 -- the debt is incurred exactly at admission. Both directions: a
// payload cannot become owed without an acceptance, which catches a debt
// appearing from a glitch or from a spurious burst_done edge.
property p_debt_only_from_admission;
  @(posedge clk) disable iff (!rst_n)
    (!$past(payload_owed) && payload_owed) |-> $past(req_accept);
endproperty
assert property (p_debt_only_from_admission);

// P3 -- the record is immutable while pending. If bank, column or tag
// could change under an outstanding write, the data would be delivered
// against metadata that is not the metadata that was requested -- and the
// write would land somewhere nobody asked for, silently.
property p_record_stable_while_pending;
  @(posedge clk) disable iff (!rst_n)
    (pending && $past(pending) && !$past(req_accept))
      |-> (pending_tag  == $past(pending_tag))
          && (pending_bank == $past(pending_bank))
          && (pending_col  == $past(pending_col));
endproperty
assert property (p_record_stable_while_pending);

// P4 -- a transaction must not retire while it still owes data. This is
// the write-specific ordering, and note it is written as a REPORT rather
// than enforced in the RTL: the block does not generate either input, and
// silently refusing an early retire would hide a defect upstream instead
// of exposing it.
property p_no_retire_while_payload_owed;
  @(posedge clk) disable iff (!rst_n)
    retire |-> !payload_owed;
endproperty
assert property (p_no_retire_while_payload_owed);

What these prove. P1 pins the precondition. P2 makes the debt's provenance exact. P3 is the most valuable — mutated metadata means data delivered to a location nobody requested, and the failure is silent because both the request and the delivery look well-formed. P4 is the one with no read analogue: it forbids declaring a write finished while its data is still in the controller.

What these do not prove. Nothing here proves the data will ever be delivered, or delivered on time, or that the right data was delivered — this block has no notion of any of those and the properties deliberately cannot express them. Nothing proves the write is finished when it retires: retirement is an input, and whether every obligation including recovery was discharged is Chapter 11.4's and 11.5's. Nothing proves the classification was right — Chapter 9.3's properties establish that. Nothing proves timing legality (Modules 13 and 14). And nothing proves anything physical: no property here concerns the array, the bus, a strobe or a voltage.

8. DV — The Expectation Runs the Other Way

A read environment builds an expectation and waits to see whether it is met. A write environment builds an expectation and must check that the controller met it — the roles are reversed, and that reshapes the monitor.

Four requirements, three of which have no read counterpart:

Create the expected transaction at admission, carrying the payload. Chapter 10.1 §8's rule applies, with an addition: the monitor must capture what data was supposed to be written, because the whole point of the eventual check is comparing what arrived at the device against what the requester asked for. A read monitor captures an address; a write monitor captures an address and a value.

Capture the open-row context at admission. Same reason as a read: the row lives in bank state, and that state can change before the data is delivered. The exposure is worse for writes, because a read resolved against a stale row returns wrong data to one requester, while a write resolved against a stale row destroys data belonging to someone else.

Model the debt, and age it. An outstanding write whose payload was never delivered is the write-specific failure, and it is invisible to any check that only fires on delivered data. The monitor needs a notion of a debt that has aged, which is the direct analogue of Chapter 10.1 §8's aged expectation and matters more, because a read that never returns stalls a requester while a write that never delivers may leave the device writing something arbitrary.

Do not update the reference memory at admission. This is the question Chapter 11.3 §8 takes up properly, and the wrong answer is tempting: a monitor that writes its model at admission has a model that diverges from the device for the whole launch interval, and will report a subsequent read of that location as an error when the device is correct.

9. Debugging — A WRITE Was Issued and DQ Never Drove

Symptom. A WRITE command is visible on the command interface with correct operands. The data bus is never driven. No write data reaches the device. A subsequent read of that location returns stale data.

This symptom is write-specific and has no read analogue, which is itself the first diagnostic: the controller failed to do something rather than failed to notice something.

Candidate mechanisms.

  1. The request was never admitted — the command on the interface came from somewhere else, or a rejection was ignored. req_accept never asserted.
  2. The request was admitted and the launch path never ran, so payload_owed is still high. Chapter 11.2.
  3. The payload was delivered to the PHY and the PHY never drove the bus — an ownership or enable problem. Chapter 6.9.
  4. The bus was driven and the probe is on the wrong signals, or at the wrong time — the launch happened outside the window being observed.
  5. A reset occurred between command issue and launch, clearing the debt while the device was still expecting data.

Evidence to collect. req_accept, pending and payload_owed at and after the command. reject_not_hit and reject_busy for the whole window. Whether the PHY write interface ever saw a valid payload. Whether any bus ownership was asserted. And whether a reset occurred at any point since admission — which mechanism 5 makes essential and which a command-only trace never shows.

Discriminator — payload_owed routes almost all of it.

  • Was req_accept ever high? If not, mechanism 1, and the rejection outputs say which kind. One signal, and it eliminates the most.
  • Is payload_owed still high? Mechanism 2 — the controller admitted the write and never delivered. The fault is in the launch path, entirely above the PHY. This is the single most decisive observation in the chapter, and it is a flag that exists only because writes owe things.
  • Is payload_owed low with no bus activity? The controller believes it delivered and the bus shows nothing — mechanism 3, and the fault is at or below the PHY boundary. Different layer, different tools.
  • Did a reset occur in the window? Mechanism 5, and its signature is distinctive: the command is on the interface and no controller state remembers it. The device may still be expecting data.
  • If payload_owed went low and the bus was driven, mechanism 4 — the observation is wrong, not the design. Check what window was probed against the configured launch relationship.

Responsible layer. Mechanisms 1, 2 and 5 are the controller — layer A in Chapter 10.1 §6's taxonomy. Mechanism 3 is the PHY boundary or below. Mechanism 4 is the observation. The payload_owed flag splits the controller cases from the PHY cases in one reading, which is the practical argument for making the debt an explicit output rather than an implied state.

Fix. Per mechanism. And in every case, expose the debt. A controller whose undelivered writes are invisible forces every such failure to be debugged from the bus inward, which is the expensive direction.

10. Common Misconceptions

"WRITE stores data when the command is issued."

Why it is tempting: the command names the location and the requester has already handed over the data, so the operation feels complete.

Concrete failure: a controller that frees the requester's buffer at command issue has nothing left to deliver, and the device writes whatever was on the bus.

Correct model: the command and the data are separate events on separate wires at separate times. §4, and the whole of Chapter 11.2.

Prevention: the payload_owed flag. A model with an explicit debt cannot express "already stored."

"WRITE activates the row."

Why it is tempting: a write is the operation the user asked for, so it should do everything required.

Concrete failure: a write issued to a closed bank — a well-formed command with an absent precondition.

Correct model: Chapter 9.1 — only an activate opens a row.

Prevention: P1. Admission requires a hit, structurally.

"WRITE carries the full row address."

Why it is tempting: it is an addressed operation with address bits on the wire.

Concrete failure: a monitor decodes a row from the column command's operands — which on a DDR4 write includes the auto-precharge flag — and attributes the data to the wrong location.

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

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

"Writes are reads backwards."

Why it is tempting: the command is identical in form and the data simply goes the other way.

Concrete failure: a write environment built by mirroring a read environment has no notion of an undelivered payload and no notion of an obligation that outlives the data — so it cannot detect either of the two failures unique to writes.

Correct model: the command rows of §2's table match and every data row reverses, and the transaction does not end at the final beat.

Prevention: build the debt and the post-delivery obligation first. A mirrored read model has neither.

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

Why it is tempting: acceptance is the last thing the requester's logic does.

Concrete failure: resources freed at acceptance, with the data still undelivered.

Correct model: acceptance creates the transaction and incurs a debt. §6's waveform shows three different durations for one write.

Prevention: §6, and the three-event table in §3.

"The PHY knows which store this is."

Why it is tempting: the PHY carries the data, so it feels like it knows the transaction.

Concrete failure: an engineer looks for a transaction identifier on the write interface and cannot find one, or a verification environment expects one.

Correct model: no transaction identity crosses a DDR interface in either direction. Chapter 10.3 §6 established this for reads; the write direction is the same. The tag is a controller-side construct.

Prevention: Chapter 10.1 §6's three-layer table, which applies unchanged.

11. Interview Reasoning

"Why must the target row already be open for a write?"

Because a write names a column within a row the bank is already holding, and carries no row address — there is nowhere on the command for one to go. The row was deposited into bank state by an earlier activate. The consequence is the same as for a read: a write to a closed bank is not slow, its precondition is absent, and a controller must model row state to know whether a write is even meaningful. What differs is the cost of getting it wrong — a read resolved against the wrong row returns bad data to one requester, while a write resolved against the wrong row destroys somebody else's data.

"What changes between a read and a write?"

The command direction is identical; every data property reverses. For a write the controller sources DQ and sources DQS, so it must produce data at a defined moment rather than watch for it. That has an asymmetric failure mode: a read that the controller mistimes misses data that still exists in the device, and is recoverable and detectable. A write that the controller mistimes causes the device to sample whatever was on the bus — there is no missed write, only a write of something else. And the transaction does not end when the data is gone, because the device still has work to do before the row can safely be closed.

"What state must a controller retain between write acceptance and completion?"

An identity, the metadata including the open-row context captured at admission, an expectation of when and how much data to deliver — and the payload itself, or a handle to it. That last item is the one with no read analogue and the one with a real resource cost: at burst widths the data is not trivial to hold. And the record must outlive the delivery, because the transaction is not finished when the last beat leaves; what remains is an obligation the device holds, which the controller must still represent.

"A write command is on the bus and DQ never drives. Where do you look?"

At whether the controller still believes it owes the payload. That one flag splits the problem across a layer boundary: if the debt is outstanding, the controller admitted the write and never launched it, and the fault is entirely above the PHY. If the debt was discharged and the bus shows nothing, the controller handed the payload over and the PHY did not drive — a different layer, different tools, usually different people. Without that flag the failure has to be debugged from the bus inward, which is the expensive direction, and it is the practical argument for making the obligation an explicit output rather than an implied state.

"Why is 'accepted' a dangerous word in this module?"

Because it names three different events at three different times. The controller admits a request; the DRAM accepts a command; the PHY consumes data. Each can happen without the next, and each is owned by a different chapter. A request can be admitted and its command never issued because timing did not permit it; a command can be accepted and its data never delivered, which is the write-specific failure; and data can be delivered while the write is still not finished. An engineer who says "the write was accepted" has said almost nothing until they say which event they mean.

12. Engineering Exercise

NUM_BANKS = 4, COL_W = 10, TAG_W = 2.

1. A request for bank 2 arrives while bank 2 holds a different row. What happens, and what must occur before it can be admitted?

2. Trace pending and payload_owed through: accept · burst_done · accept · retire · accept · burst_done · retire.

3. Name the three distinct events this module calls "accepted," and give one failure that can occur between each adjacent pair.

4. A controller frees the requester's data buffer at req_accept. Give the failure and say why it is worse than the read-side equivalent.

5. A monitor updates its reference memory at admission. Construct the sequence that makes it report a false error.

6. Write the property that catches a transaction retiring while its payload is still owed, and say why the RTL reports rather than prevents it.

13. Summary

A read's controller waits; a write's controller owes. Accepting a write creates an obligation to produce data at a defined moment, which is a different kind of object from an obligation to recognise something that will arrive.

The command rows of the read/write table match and every data row reverses. The controller sources DQ and DQS, so it must launch rather than watch — and the failure modes reverse with them: reads fail by absence, writes fail by commission. A mistimed read misses data that still exists; a mistimed write commits something arbitrary into a real location.

Three different events are called "accepted": the controller admits a request, the DRAM accepts a command, the PHY consumes data. They happen at three times, each can occur without the next, and each belongs to a different chapter.

The transaction carries one thing a read's does not — a payload it still owes. That debt can be defaulted on, occupies real resources, and is not discharged by issuing the command.

And it does not end when the data is gone. payload_owed falls before pending does, and the gap between them is an obligation the device holds. This chapter cannot explain that gap; it exists to make sure you noticed it.

Which makes the debt worth exposing. One flag splits an undelivered write from a PHY that did not drive — controller fault from boundary fault — in a single reading, and without it every such failure is debugged from the bus inward.

14. What Comes Next

The controller owes a payload and has not been told when to deliver it. Chapter 11.2 — Write Latency (CWL) is that relationship.

Chapter 10.2 established what a read latency relates and why the unit discipline matters, and this chapter's interval is its counterpart — but it is not its mirror. A read latency is a promise the device makes: issue a read and data will appear at a defined offset. A write latency is a requirement the controller must meet: issue a write and you must present data at a defined offset, or the device samples something else.

A promise you can mispredict and recover from. A requirement you can only fail.

That chapter derives why the relationship must exist at all before naming it, separates it carefully from CL and from the recovery interval that follows the burst, and builds the pipeline that carries this chapter's payload across it.


Return to Write (WR / WRA) for the command and its observed/accepted/completed decomposition, The Read Command for the transaction this one deliberately mirrors, Row Hits for the classification a write requires, Column Address for why no row travels with a write, and DM for masking, which travels with the beat rather than the command.

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.