Skip to content
VLSI Mentor

DDR · Module 19

Serialisation

A controller runs wide and slow because that is what synthesisable logic can do; the pins run narrow and fast because that is what the bus is. The gearbox converts between them — and a correct gearbox proves nothing about sampling.

Chapter 19.1 §4 named four paths inside the PHY and opened none of them. This chapter opens two — the write path and the read path — at the point where they do the same job in opposite directions.

The job has a simple statement and a consequential one.

The controller side is wide and slow. The pin side is narrow and fast. Something has to convert between them, and that conversion owns an ordering contract that is entirely digital and entirely capable of being wrong.

The consequential part is the second half. Serialisation is the one piece of the PHY that a digital designer really does write, really can verify, and really does get wrong — and it is also the piece most often mistaken for evidence that the physical interface works.

1. Why the Two Sides Cannot Match

Start with the constraint that forces the whole structure.

A memory controller is synthesised logic. Its clock frequency is set by the slowest timing path through it — queues, arbitration, address decode, the long combinational chain Chapter 17.1 §14 described. Push that clock higher and the design stops closing timing.

The DDR pins have no such constraint, because what happens there is not synthesised logic. The interface transfers data on both edges of a strobe at a rate set by the standard and the speed bin, and the structures that do it are the technology-specific cells Chapter 19.1 §5 listed.

So the two sides run at genuinely different rates, and the only way to reconcile a slow side with a fast side is to make the slow side wider.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  same data, two shapes

    controller side   |<------------ one wide word ------------>|
                      one transfer, slow clock

    pin side          |beat|beat|beat|beat|beat|beat|beat|beat|
                      several narrow transfers, fast rate

This is the same trade Chapter 4.2 identified inside the DRAM, applied at a different boundary. The DRAM widens its internal array access so its core can run slower than its pins; the SoC widens its controller datapath so its logic can run slower than the interface. Two instances of one idea, and recognising that is worth more than memorising either.

2. This Is Not the Prefetch Serialiser

The two conversions are close enough that conflating them is easy, and the differences are exactly the things that matter for building one.

4.2's prefetch_serializerthis chapter's PHY gearbox
Which side of the interfaceinside the DRAMinside the PHY, in the SoC
What it convertsarray core word → DQ beatscontroller word → pin-side beats
What sets the ratiothe generation's prefetch depththe controller-to-PHY clock ratio
Who chooses the ratiothe DDR standardthe SoC architect, within what the interface supports
Backpressurenone — the core must present data on demandyes — the controller can be stalled
Failure modethe burst abortsa word is dropped or reordered

The backpressure row is the important one. prefetch_serializer models a device whose array must deliver on demand, because a DRAM cannot tell its own pins to wait. A PHY gearbox sits between two blocks that can each stall the other, so it owns a handshake — and handshakes are where ownership bugs live (Chapter 17.5 §2).

On the ratio, this chapter invents nothing. The industry-standard DDR PHY Interface (DFI) defines the controller-to-PHY clock relationship and supports 1:1, 1:2 and 1:4 ratios, so that a controller may run at the PHY's rate, half of it, or a quarter. Those are the real values the trade takes in practice, and the worked configuration below is labelled as an educational instance of that idea rather than a universal figure.

3. The Configuration

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  EDUCATIONAL RATIO — NOT A UNIVERSAL DDR PHY IMPLEMENTATION.
  Chosen small so a word can be traced by hand.

    LANE_W        8 bits     one byte lane's DQ width
    LANES         2          two byte lanes
    BEATS         4          pin-side transfers per controller word
    ------------------------------------------------------------
    beat width    LANE_W x LANES        = 16 bits  (pin side)
    word width    LANE_W x LANES x BEATS = 64 bits (controller side)

Real systems commonly use eight beats (Chapter 12.1's burst length) and more lanes — a DDR4 DIMM presents a 72-bit channel, 64 data bits plus 8 for ECC, while a DDR5 DIMM splits the module into two independent 32-bit subchannels, 40 bits each with ECC. The lane count follows from that and differs by generation and module type, which is why the parameter exists rather than a constant.

Packing order is a contract, not a convention. With BEATS = 4, beat 0 must carry a defined slice of the 64-bit word, and the controller and the PHY must agree which. This chapter's blocks use beat 0 = least-significant slice, ascending — stated explicitly because the opposite choice is equally valid and a silent disagreement produces §14's first debugging symptom.

4. The Gearbox, Both Ways

The two gearboxes in the PHY datapath, shown as mirror images. On the write path, the controller presents a wide word across a ready valid handshake; the write gearbox takes ownership of that word, then emits it as a sequence of narrow beats toward an abstract transmit boundary named phy tx beat, which may stall it. On the read path, an abstract capture block owned by module twenty delivers narrow captured beats that cannot be stalled; the read assembler places each beat into a widening word and, when the final beat arrives, presents the completed wide word to the controller across a ready valid handshake that can stall. The asymmetry is marked: the transmit side can be back-pressured from the pins, whereas the receive side cannot, because the data arrives when the device sends it.Controller wordwide · slow · stallableWrite gearboxowns one word at a timephy_tx_beatABSTRACT boundaryDQ pinsModule 22 — electricalCAPTUREModule 20 opens thiscaptured_beatABSTRACT · cannot stallRead assemblerplaces beats by indexController wordwide · stallableThe asymmetryTX stalls · RX cannot12

5. The Write Gearbox

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ─────────────────────────────────────────────────────────────────────
// phy_write_gearbox
//
// CLASSIFICATION
//   Synthesizable educational RTL. Sequential. One responsibility:
//   accept ONE wide controller word, take ownership of it, and emit it
//   as BEATS narrow transfers in a defined order, honouring downstream
//   backpressure.
//
//   It models ORDERING and OWNERSHIP. It does not model timing.
//
// WHAT IT DOES NOT MODEL
//   - NOT the DRAM's prefetch conversion. Chapter 4.2's
//     prefetch_serializer models the device's array-to-pin width trade;
//     this is the PHY's controller-facing conversion (§2).
//   - No physical transmit. phy_tx_beat is an ABSTRACT boundary. Pin
//     drive, slew, edge placement, double-edge transfer and the strobe
//     that accompanies the data are all outside this model; Chapter
//     19.4 owns strobe generation and Module 22 the electrical side.
//   - No burst ORDERING policy. Whether a burst is sequential or
//     interleaved is a device mode register; Chapters 12.2 and 12.3
//     own it. This block emits ascending beat indices and the mapping
//     from index to column order belongs upstream.
//   - No DQS, no delay elements, no training, no clock-domain
//     crossing. One clock domain throughout; §13 explains why.
// ─────────────────────────────────────────────────────────────────────
module phy_write_gearbox #(
  parameter int LANE_W = 8,
  parameter int LANES  = 2,
  parameter int BEATS  = 4,
  parameter int BEAT_W = LANE_W * LANES,
  parameter int WORD_W = BEAT_W * BEATS,
  // Index over beats. Guarded: $clog2(1) is 0 and a zero-width index
  // is not a legal declaration.
  parameter int IDX_W  = (BEATS <= 1) ? 1 : $clog2(BEATS)
) (
  input  logic                clk,
  input  logic                rst_n,

  // ── Controller side. Producer: controller. Consumer: this block.
  //    Transfer on word_valid && word_ready at the rising edge.
  input  logic                word_valid,
  input  logic [WORD_W-1:0]   word_data,
  output logic                word_ready,

  // ── Pin side, ABSTRACT. Producer: this block. Consumer: whatever
  //    drives the pins. Transfer on beat_valid && beat_ready.
  //    The physical I/O implementation is intentionally outside this
  //    RTL model -- see the header.
  output logic                beat_valid,
  output logic [BEAT_W-1:0]   beat_data,
  output logic [IDX_W-1:0]    beat_index,
  output logic                beat_first,
  output logic                beat_last,
  input  logic                beat_ready,

  output logic                busy,
  output logic                err_overwrite_attempt
);

  if (LANE_W < 1) $fatal(1, "phy_write_gearbox: LANE_W must be >= 1");
  if (LANES  < 1) $fatal(1, "phy_write_gearbox: LANES must be >= 1");
  if (BEATS  < 1) $fatal(1, "phy_write_gearbox: BEATS must be >= 1");

  logic                held;
  logic [WORD_W-1:0]   word;
  logic [IDX_W-1:0]    idx;

  // ── A beat transfers on the handshake. Everything below advances on
  //    this event and on nothing else -- not on beat_valid, not on
  //    beat_ready alone. Chapter 17.5 §2's contract.
  logic beat_fire;
  assign beat_fire = beat_valid && beat_ready;

  assign beat_last  = held && (idx == IDX_W'(BEATS - 1));
  assign beat_first = held && (idx == '0);

  // ── Ready when empty, or when the final beat is completing this
  //    cycle. The second term is what makes back-to-back words
  //    possible with no idle cycle between them; without it every word
  //    costs BEATS+1 cycles instead of BEATS.
  //    Note the dependence direction: word_ready never looks at
  //    word_valid, so no combinational loop can form.
  assign word_ready = !held || (beat_fire && beat_last);

  assign busy       = held;
  assign beat_valid = held;

  // ── The beat slice. A variable part-select over the held word, with
  //    beat 0 as the least-significant slice (§3's stated contract).
  assign beat_data  = word[idx * BEAT_W +: BEAT_W];

  // ── Should never assert: a word offered into an owned slot that is
  //    not completing. Surfaced rather than silently dropped.
  assign err_overwrite_attempt =
      word_valid && held && !(beat_fire && beat_last);

  always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
      held <= 1'b0;
      idx  <= '0;
      word <= '0;
    end else begin
      // ── Release on the final beat, then capture. Program order
      //    matters: a simultaneous last-beat and new-word accept must
      //    be a clean handover, not a dropped word. §7's cycle 6.
      if (beat_fire && beat_last) begin
        held <= 1'b0;
        idx  <= '0;
      end else if (beat_fire) begin
        idx  <= idx + IDX_W'(1);
      end

      if (word_valid && word_ready) begin
        held <= 1'b1;
        word <= word_data;
        idx  <= '0;
      end
    end
  end

endmodule

BEATS = 1 is worth checking by hand. IDX_W is guarded to 1, idx is always zero, beat_first and beat_last are both high on the single beat, and word_ready is high whenever that beat is firing. One word, one beat, no stall state — the degenerate case works without a special path, which is the test of whether the general expression was written correctly.

6. The Read Assembler

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ─────────────────────────────────────────────────────────────────────
// phy_read_assembler
//
// CLASSIFICATION
//   Synthesizable educational RTL. Sequential. One responsibility:
//   place arriving captured beats into a widening word by index, and
//   present the completed word to the controller.
//
// WHAT IT DOES NOT MODEL
//   - CAPTURE. captured_valid / captured_data arrive from an ABSTRACT
//     capture block. How DQ is sampled on DQS edges, where the
//     sampling point sits, and what makes it correct are Module 20's
//     subject entirely. This block consumes the result and asks no
//     questions about its provenance.
//   - No backpressure toward capture, BY DESIGN. A DRAM cannot be told
//     to pause mid-burst (§4), so there is no ready output here. The
//     consequence is the overrun flag below, which is a real
//     limitation of a single-buffer design and is stated rather than
//     hidden.
//   - No strobe gating (Chapter 19.4), no delay elements, no training,
//     no read framing policy -- Chapter 10.5's phy_read_boundary owns
//     the controller-facing framing contract and is reused.
// ─────────────────────────────────────────────────────────────────────
module phy_read_assembler #(
  parameter int LANE_W = 8,
  parameter int LANES  = 2,
  parameter int BEATS  = 4,
  parameter int BEAT_W = LANE_W * LANES,
  parameter int WORD_W = BEAT_W * BEATS,
  parameter int IDX_W  = (BEATS <= 1) ? 1 : $clog2(BEATS)
) (
  input  logic                clk,
  input  logic                rst_n,

  // ── From the ABSTRACT capture block. No ready: these arrive when
  //    the device sends them.
  input  logic                captured_valid,
  input  logic [BEAT_W-1:0]   captured_data,
  // Asserted with the first beat of a burst. Supplied by the capture
  // side because only it knows where the burst began -- deriving it
  // from a local counter is §14's one-beat-shift bug.
  input  logic                captured_first,

  // ── Controller side. THIS side can stall.
  output logic                word_valid,
  output logic [WORD_W-1:0]   word_data,
  input  logic                word_ready,

  output logic [IDX_W-1:0]    fill_index,
  output logic                assembling,
  // A new burst arrived while a completed word was still unconsumed.
  // Data is lost. Sticky until reset so a transient cannot be missed.
  output logic                err_overrun
);

  if (LANE_W < 1) $fatal(1, "phy_read_assembler: LANE_W must be >= 1");
  if (LANES  < 1) $fatal(1, "phy_read_assembler: LANES must be >= 1");
  if (BEATS  < 1) $fatal(1, "phy_read_assembler: BEATS must be >= 1");

  logic [WORD_W-1:0] acc;
  logic [IDX_W-1:0]  idx;
  logic              active;
  logic [WORD_W-1:0] out_word;
  logic              out_full;

  logic word_fire, last_beat;
  assign word_fire = word_valid && word_ready;
  // The final beat is the one landing at the top index while a burst
  // is in progress, or -- for BEATS == 1 -- the first beat itself.
  assign last_beat = captured_valid
                  && ((active && idx == IDX_W'(BEATS - 1))
                      || (captured_first && BEATS == 1));

  assign word_valid = out_full;
  assign word_data  = out_word;
  assign fill_index = idx;
  assign assembling = active;

  always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
      acc         <= '0;
      idx         <= '0;
      active      <= 1'b0;
      out_word    <= '0;
      out_full    <= 1'b0;
      err_overrun <= 1'b0;
    end else begin
      // ── Consume first, so a completed word leaving this cycle frees
      //    the slot for one completing in the same cycle.
      if (word_fire) out_full <= 1'b0;

      if (captured_valid) begin
        if (captured_first) begin
          // A new burst always restarts the accumulator at index 0.
          // Trusting the capture side's framing rather than a local
          // counter is what keeps a dropped beat from shifting every
          // subsequent word (§14).
          acc[0 +: BEAT_W] <= captured_data;
          idx              <= IDX_W'(1);
          active           <= 1'b1;
        end else if (active) begin
          acc[idx * BEAT_W +: BEAT_W] <= captured_data;
          idx                         <= idx + IDX_W'(1);
        end
        // A beat with neither captured_first nor an active burst is
        // stray -- outside any burst this block knows about. It is
        // dropped deliberately rather than placed at a guessed index.

        if (last_beat) begin
          // Completion. The word being finished is the accumulator
          // with this final beat merged in; taking acc directly would
          // publish the word one beat short.
          out_word <= (BEATS == 1)
                    ? WORD_W'(captured_data)
                    : ((acc & ~(WORD_W'({BEAT_W{1'b1}}) << (idx * BEAT_W)))
                       | (WORD_W'(captured_data) << (idx * BEAT_W)));
          active   <= 1'b0;
          idx      <= '0;

          // Overrun: the output slot is still occupied and is not
          // being consumed this cycle. The previous word is lost.
          if (out_full && !word_fire) err_overrun <= 1'b1;
          else                        out_full    <= 1'b1;
        end
      end
    end
  end

endmodule

Why captured_first is an input rather than a counter. A local counter that assumed every BEATS-th beat starts a word would resynchronise incorrectly after a single dropped or spurious beat, and every subsequent word would be shifted — §14's second debugging symptom, and one of the nastiest bugs in this area because the data is not corrupt, merely misaligned. Taking framing from the side that knows where the burst began confines the damage to one burst.

7. The Write Path, With a Stall

EDUCATIONAL — cycle numbers show ordering only. No pin timing is implied. BEATS = 4. The controller presents word A (beats A0..A3), the pin side stalls mid-word, then word B follows immediately.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  cyc  w_valid w_ready held idx  b_valid b_data b_ready b_fire first last
  ───  ─────── ─────── ──── ───  ─────── ────── ─────── ────── ───── ────
   0      1       1     0    0      0      --      1       0     0     0
   1      1       0     1    0      1      A0      1       1     1     0
   2      1       0     1    1      1      A1      1       1     0     0
   3      1       0     1    2      1      A2      0       0     0     0   <-- STALL
   4      1       0     1    2      1      A2      0       0     0     0
   5      1       0     1    2      1      A2      1       1     0     0
   6      1       1     1    3      1      A3      1       1     0     1   <-- last + accept
   7      1       0     1    0      1      B0      1       1     1     0
   8      1       0     1    1      1      B1      1       1     1     0

Three rows carry the lesson.

Cycles 3–5 — the stall. beat_ready is low, so beat_fire is low, so idx does not advance and beat_data holds A2 unchanged across three cycles. Nothing is lost and nothing is repeated: the beat is simply offered until it is taken. Stability while stalled is a contract, not a convenience — a consumer that latches on beat_valid alone would take A2 three times.

Cycle 6 — the handover. The final beat of A fires and word B is accepted, on one edge. word_ready was high precisely because beat_fire && beat_last was true. This is the term that makes back-to-back words cost BEATS cycles rather than BEATS + 1, and it is the case §5's always_ff ordering exists to make correct.

Cycle 7. B0 is already being offered. No bubble.

8. The Read Path, With Backpressure

Same configuration. Four captured beats arrive, the controller stalls, then accepts.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  cyc  cap_valid cap_first cap_data active idx out_full w_valid w_ready overrun
  ───  ───────── ───────── ──────── ────── ─── ──────── ─────── ─────── ───────
   0       1         1        R0       0    0     0        0       1        0
   1       1         0        R1       1    1     0        0       1        0
   2       1         0        R2       1    2     0        0       1        0
   3       1         0        R3       1    3     0        0       0        0
   4       0         0        --       0    0     1        1       0        0   <-- stalled
   5       0         0        --       0    0     1        1       0        0
   6       0         0        --       0    0     1        1       1        0   <-- taken
   7       0         0        --       0    0     0        0       1        0

Cycle 3 is the completing beat: R3 lands at index 3, active clears, and the word is published at cycle 4.

Cycles 4–6 — the controller stalls. word_valid stays high and word_data holds. The assembler cannot do anything else; there is no upstream to slow down.

Cycle 6 the word is taken and the slot frees.

Now the case that matters. Suppose a second burst had begun arriving at cycle 5, while the first word was still unconsumed:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   5       1         1        S0       ...   out_full=1, word_fire=0
           -> the assembler starts accumulating S, and when S completes
              it finds the output slot still occupied.
           -> err_overrun asserts. The first word is lost.

This is a real limitation of a single-buffer design, and it is stated rather than engineered around. A production PHY carries enough read buffering that the controller's acceptance rate is not on the critical path — but it carries a finite amount, so the same failure exists at a larger depth, and the controller's read-data path must be architected not to reach it. Chapter 17.5 §13 discussed the equivalent pressure at the ingress; this is the same shape at the other end of the machine.

9. What the Assertions Prove

Both blocks have their own clk and rst_n, so these may live inside the modules or in bind units.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ═══ WRITE GEARBOX ═══════════════════════════════════════════════════

// ── W1. The beat index advances only on a beat handshake. The single
//    most valuable property here: it catches an index driven by
//    beat_valid or by a free-running counter, which silently skips
//    beats whenever the pin side stalls.
property p_idx_advances_on_fire_only;
  @(posedge clk) disable iff (!rst_n)
    (idx != $past(idx, 1))
      |-> ($past(beat_fire, 1) || $past(word_valid && word_ready, 1));
endproperty
a_idx_advances_on_fire_only: assert property (p_idx_advances_on_fire_only);

// ── W2. A stalled beat is stable. §7's cycles 3 to 5. Without this a
//    consumer cannot rely on sampling the beat when it finally is
//    ready.
property p_stalled_beat_stable;
  @(posedge clk) disable iff (!rst_n)
    (beat_valid && !beat_ready)
      |=> (beat_valid && $stable(beat_data) && $stable(beat_index));
endproperty
a_stalled_beat_stable: assert property (p_stalled_beat_stable);

// ── W3. An owned word is never overwritten. The ownership property:
//    a new word may be accepted only into a free slot or one being
//    freed this cycle.
property p_no_word_overwrite;
  @(posedge clk) disable iff (!rst_n)
    (word_valid && word_ready) |-> (!held || (beat_fire && beat_last));
endproperty
a_no_word_overwrite: assert property (p_no_word_overwrite);

// ── W4. Exactly BEATS transfers per accepted word. Stated as a
//    sequence so it checks the COUNT rather than merely the endpoints,
//    which is what catches an off-by-one in the last-beat comparison.
property p_exactly_beats_per_word;
  @(posedge clk) disable iff (!rst_n)
    (word_valid && word_ready)
      |=> (beat_fire && beat_first) ##0 (beat_fire [->BEATS-1]) ##0 beat_last;
endproperty
a_exactly_beats_per_word: assert property (p_exactly_beats_per_word);

// ═══ READ ASSEMBLER ══════════════════════════════════════════════════

// ── R1. Assembly advances only on a captured beat. The receive-side
//    analogue of W1.
property p_fill_advances_on_capture_only;
  @(posedge clk) disable iff (!rst_n)
    (fill_index != $past(fill_index, 1)) |-> $past(captured_valid, 1);
endproperty
a_fill_advances_on_capture_only: assert property (p_fill_advances_on_capture_only);

// ── R2. A completed word is stable while the controller stalls. §8's
//    cycles 4 to 6.
property p_completed_word_stable;
  @(posedge clk) disable iff (!rst_n)
    (word_valid && !word_ready)
      |=> (word_valid && $stable(word_data)) || err_overrun;
endproperty
a_completed_word_stable: assert property (p_completed_word_stable);

// ── R3. A burst always restarts assembly at index 0. Catches the
//    free-running-counter framing bug of §6, whose symptom is a
//    permanent one-beat shift rather than corruption.
property p_first_beat_restarts;
  @(posedge clk) disable iff (!rst_n)
    (captured_valid && captured_first) |=> (fill_index == IDX_W'(BEATS == 1 ? 0 : 1));
endproperty
a_first_beat_restarts: assert property (p_first_beat_restarts);

// ── R4. Overrun is sticky. A transient that clears itself is a
//    transient nobody sees, and lost read data must never be quiet.
property p_overrun_sticky;
  @(posedge clk) disable iff (!rst_n)
    err_overrun |=> err_overrun;
endproperty
a_overrun_sticky: assert property (p_overrun_sticky);

// ═══ COVERS ══════════════════════════════════════════════════════════
c_tx_stall_midword:  cover property (@(posedge clk) disable iff (!rst_n)
                       beat_valid && !beat_ready && !beat_first && !beat_last);
c_back_to_back_word: cover property (@(posedge clk) disable iff (!rst_n)
                       beat_fire && beat_last && word_valid && word_ready);
c_rx_output_stall:   cover property (@(posedge clk) disable iff (!rst_n)
                       word_valid && !word_ready);
c_rx_overrun:        cover property (@(posedge clk) disable iff (!rst_n)
                       $rose(err_overrun));

What they prove. That both gearboxes preserve ordering, respect their handshakes, advance only on real transfers, never silently overwrite owned data, and produce exactly the expected number of transfers per word. These are the real bugs in real gearboxes, and W1, W4 and R3 each catch a distinct one.

What they do not prove — and this is the section's point. Not one of these properties says anything about whether a beat arrived at the pins at the right moment, whether it was sampled in the right place, or whether the eye was open when it was. W4 can pass on a design whose every beat is sampled on the wrong edge, because W4 is counting logical transfers across an abstraction and the sampling happened on the far side of it.

Concretely: an assertion proving beat order is correct is evidence about layer C in Chapter 19.1 §8's taxonomy and no evidence at all about layer D. The two are verified by different means — these properties, versus measurement on real silicon with trained settings (Module 21).

Vacuity. W4's consequent uses a [->BEATS-1] goto repetition that degenerates when BEATS == 1; the property should be guarded or specialised in that configuration rather than left to evaluate an empty repetition. c_tx_stall_midword and c_rx_overrun exist because the stall and overrun paths are the ones a simple directed test never reaches — and W2, R2 and R4 are checking nothing without them.

10. DV — Two Independent Queues

The checker must not reuse either block's index arithmetic. Chapter 8.6 §8 established the general rule; here it has a specific shape.

Write side. Maintain a queue of accepted controller words. On each observed accept, push the word and independently split it into BEATS expected slices — computed by masking and shifting, not by the RTL's part-select. On each observed beat_fire, pop the next expected slice and compare against beat_data. At end of test, assert the expected-slice queue is empty.

Read side. Maintain a queue of captured beats grouped by observed captured_first. Independently assemble each group into an expected word. On each observed word_fire, pop and compare. Count expected words against delivered words; a difference is data loss and should reconcile exactly against err_overrun assertions.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  WRITE GEARBOX BEAT MISMATCH
    word index        : 3
    accepted at cycle : 118
    word_data         : 0x1122_3344_5566_7788
    BEATS = 4, BEAT_W = 16, contract: beat 0 = least-significant slice

    expected beat sequence : 7788  5566  3344  1122
    observed beat sequence : 1122  3344  5566  7788

    first differing   : beat 0 (expected 0x7788, observed 0x1122)
    observed at cycle : 119

    diagnosis : the observed sequence is the expected one reversed.
                The beat slice is being selected from the most-
                significant end, i.e. a DESCENDING packing order
                against §3's ascending contract. Every beat differs,
                which is the signature of an ordering disagreement
                rather than a corrupted beat.

    discriminator : a full reversal and a lane swap both corrupt every
                beat, and they are distinguished by WHICH bits move.
                Re-run with word_data = 0x0000_0000_0000_00FF.
                  reversal  -> the 0xFF appears in beat 3
                  lane swap -> the 0xFF stays in beat 0 but moves to
                               the other lane's byte position
                One word settles it with no further analysis.

The check next line is what makes the report worth building: it names the one further observation that settles the diagnosis, rather than leaving the engineer to design that experiment.

Directed cases worth running: BEATS = 1; a stall on the first beat, a middle beat and the last beat, which are three different paths through word_ready; back-to-back words with no gap; a word offered while one is held and not completing, which must set err_overwrite_attempt; on the read side, a stray beat with no captured_first, a burst arriving while the output is blocked, and a reset mid-burst.

11. Corner Cases

Write gearbox.

SituationCorrect behaviourFailure if mishandled
BEATS = 1IDX_W guarded to 1; first and last on one beatzero-width index; a stall state that never exits
stall on the first beatidx holds at 0; data stablea skipped beat 0, shifting the whole word
stall on the last beatword_ready stays lowa new word accepted over the unfinished one
last beat and new accept togetherclean handover; held stays highthe incoming word dropped, or the old one repeated
word offered while held and not completingrefused; err_overwrite_attemptthe owned word silently replaced
beat_ready high with beat_valid lownothing happensa phantom beat counted
reset mid-wordownership cleared, idx zeroeda partial word resumed against a new one
LANES = 1legal — a single-lane interfacezero-width beat

Read assembler.

SituationCorrect behaviourFailure if mishandled
BEATS = 1first beat completes immediatelya word that never completes
beat with no captured_first and no active burstdropped deliberatelyplaced at a guessed index, corrupting a later word
captured_first mid-burstrestarts assembly at 0the previous partial word merged into the new one
output blocked, burst completeserr_overrun, stickysilent data loss
completion and consumption in one cycleslot frees and refillsa valid word dropped
reset mid-burstaccumulator and index cleareda stale beat in the next word
capture-side ready outputthere is none — by designmodelling a DRAM that can be paused

The last row is not a corner case so much as a design boundary, and it is in the table because a reviewer who asks “why is there no ready here?” deserves the answer in the same place as the other answers.

12. Synthesis and What Is Missing

Cost. The write gearbox is WORD_W flops for the held word, IDX_W for the index, one valid flag, and a BEATS-to-one multiplexer of width BEAT_W. For the chapter's configuration that is 64 + 2 + 1 flops and a 4:1 16-bit mux. The read assembler is comparable, plus a second WORD_W register for the output buffer.

The multiplexer is what scales. With eight beats and a wider interface the beat mux grows with BEATS × BEAT_W, and it sits on the path toward the I/O. In a real PHY this is one of the places where the digital logic starts to be shaped by the physical implementation rather than by the architecture.

What a production write path has that this does not: write data masking, which Chapter 6.7 owns and which must be serialised alongside the data with the same ordering contract; per-lane data paths rather than one monolithic word, so that each lane can carry its own trained timing (19.5); the strobe generation that accompanies the data (19.4); and the actual double-edge transmit, which is not a logic function.

What a production read path has that this does not: far more buffering than one word; per-lane assembly, because lanes do not arrive aligned to each other before deskew; and the capture machinery itself, which is Module 20's entire subject.

13. One Clock Domain, and Why

Both blocks use a single clock. A real PHY does not, and the honest thing is to say why the model is single-domain rather than to pretend the question does not arise.

The real structure. The controller side runs at the controller's clock. The pin side runs at a rate related to it by the ratio of §2 — and in a 1:2 or 1:4 system those are genuinely different frequencies, typically phase-related rather than independent. A real gearbox therefore straddles a boundary, and the crossing is managed by structures designed for it.

Why this chapter does not model it. A clock-domain crossing done casually — sampling a signal from another domain in an always_ff — is not a simplification of a correct design, it is a wrong design that simulates. Modelling the crossing properly would require a synchroniser or an asynchronous FIFO, and neither would teach anything about serialisation that the single-domain model does not, while both would add a large amount of machinery that is not this chapter's subject.

So the abstraction is stated instead of faked: the beat boundary is where the domain change happens in a real PHY, and this model places both sides in one domain to isolate the ordering contract. That is a modelling decision, not a claim about hardware, and a reader who carries the single-domain structure into a real design will find the first thing that breaks is the thing this section is warning about.

14. Debugging

Symptom: every write burst has its beats in reverse order. Layer C — deterministic and rate-independent. The discriminator is §10's report: check whether beat 0 carries the least- or most-significant slice, and compare against the contract the consumer assumes. This is a contract disagreement more often than a coding error, and the fix belongs wherever the two sides were specified, not necessarily in the gearbox.

Symptom: read data is correct but shifted by exactly one beat. Do not start with DQS. A one-beat shift is the signature of a framing problem: a captured_first that arrives a cycle early or late, or a local counter used instead of it (§6). Check whether the shift is permanent — a framing bug shifts every subsequent word, while a capture problem produces corruption that varies. R3 is the property that catches the framing version.

Symptom: occasional lost read words under heavy traffic. Check err_overrun first. A single-buffer assembler overruns whenever the controller stalls across a burst boundary, and the fix is buffering or backpressure further up, not anything in the capture path.

Symptom: the gearbox works in simulation and the interface does not work at all. Expected, and not a contradiction. §9 states exactly what the assertions do not cover. Move to Chapter 19.1 §8's taxonomy and ask the rate question: if a lower data rate works, the ordering logic is exonerated and the problem is in layers D, E or F.

Symptom: one byte lane produces wrong data and the others are fine. Almost never a gearbox bug, because the gearbox slices a word uniformly and a slicing error affects every lane identically. A single-lane fault points at per-lane state — trained delays (19.5) — or at the board (Module 22).

15. Misconceptions

“The PHY is just a serialiser.” Serialisation is two of the four paths in 19.1 §4, and the receive side additionally needs gating, capture and trained delay. Clue: a PHY model with no receive-side state.

“A serialiser RTL example models pad timing.” §7's callout. Every cycle in these traces is an ordering label. Clue: sign-off on the interface based on gearbox simulation.

“An assertion on beat order proves the data was sampled correctly.” §9 — W4 passes on a design sampling every beat on the wrong edge. Clue: a verification plan with no post-silicon margin measurement.

“One controller clock equals one DDR transfer.” That is the 1:1 case and it is one of at least three the standard interface supports. Clue: a bandwidth calculation that multiplies the controller clock by two.

“The serialisation ratio is universal.” §2 — 1:1, 1:2 and 1:4 are all standard, and the choice is the SoC architect's. Clue: a design document stating the PHY ratio with no configuration named.

“This is the same thing as prefetch.” §2's table. Same idea, different side of the interface, different ratio, and only one of them has backpressure. Clue: a design that expects the DRAM's prefetch depth to determine the controller's data width.

“The read side can apply backpressure.” §4 — there is no signal that pauses a DRAM mid-burst. Clue: a read path with a ready output pointed at capture.

“Byte lane means one byte of controller data.” A lane is a group of DQ pins with its own strobe; the controller word spans all lanes across all beats. With the chapter's configuration, one lane contributes BEATS bytes to each word, not one. Clue: a lane-to-byte mapping derived without reference to the beat count.

“Correct simulation guarantees hardware operation.” Everything in 19.1 §5 is outside the simulation. Clue: surprise when a functionally-verified PHY fails bring-up.

16. Interview Reasoning

“Why does a PHY serialise at all?” Because the controller's clock is limited by synthesised timing paths and the interface's rate is not, so the only reconciliation is width. The strong answer names why the controller cannot simply run faster.

“What ratio does a DDR PHY use?” It is a design choice, and the standard interface supports 1:1, 1:2 and 1:4. Naming a single number is the weak answer.

“Why can the transmit side be stalled and the receive side not?” Because read data arrives on a schedule the device is executing, and there is no mechanism to pause it. Then the consequence: the receive side must buffer or lose data.

“What can SVA prove about a serialiser?” Ordering, ownership, transfer counts, stability under stall. What can it not prove? Anything about sampling position or margin. Volunteering the second half unprompted is the discriminator.

“Read data is shifted by one beat. Where do you look?” Framing, not capture — and the discriminator is whether the shift is permanent or intermittent.

“How would you verify a gearbox without copying its logic?” An independent queue of expected slices computed by mask and shift, compared against observed beats, with the queue asserted empty at end of test.

“Your gearbox is correct and the interface does not work. What have you learned?” That layers A through C are probably fine — which is real information — and that the problem is in capture, training or signal integrity. Knowing the taxonomy is the answer.

17. Exercises

1. With LANE_W = 8, LANES = 4, BEATS = 8, give the beat width and the controller word width. How many bytes does a single lane contribute to one word?

2. Under §3's ascending contract, which bits of a 64-bit word does beat 2 carry when BEATS = 4? Which bits would it carry under the reverse contract?

3. Trace §7's stall with the stall moved to the first beat instead of the third. Which cycle does the word complete on, and does word_ready behave differently?

4. Remove (beat_fire && beat_last) from word_ready. How many cycles does each word now cost, and which cover stops hitting?

5. Write W1 for the read assembler without using fill_index. Which signal must it reference instead, and why is that harder to get right?

6. Construct a capture sequence that makes err_overrun assert with BEATS = 1. How many cycles of controller stall are required?

7. A colleague replaces captured_first with a modulo-BEATS counter. Give the beat sequence that breaks it and describe the symptom in the controller's data.

8. Explain why W4 passing tells you nothing about layer D in 19.1 §8's taxonomy. Name the property you would need instead and say why it cannot be written in RTL.

18. Where This Goes

Both gearboxes are built, and both stop at a named abstraction — phy_tx_beat going out, captured_beat coming in.

What neither of them knows is when a beat should leave. A gearbox emits beats as fast as its consumer takes them; nothing in this chapter decides the relationship between a command launched on the command/address path and the data that belongs to it. That relationship is the PHY's alignment problem, and Chapter 19.3 owns it.

19.4 then supplies the strobe that accompanies transmitted data and the gate that protects the receive path, and 19.5 explains where the settings behind both come from. The abstraction marked captured_beat stays closed until Module 20.

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.