Skip to content
VLSI Mentor

DDR · Module 24

LPDDR4 Architecture

Six command pins per channel, commands over one, two or four positive edges, and two channels that must not interact. Counting the pins for a x32 interface gives twelve against the previous generation's ten.

Chapter 24.2 §20 left one thing assumed throughout: that a REF, an SRE or an ACT simply arrives at the device. On this family it does not simply arrive, and the mechanism by which it arrives is not what any DDR chapter in this curriculum would lead you to expect.

Chapter 4.7 §5 established that LPDDR4 organises a device as two independent narrow channels, and why a phone's requester mix makes that attractive. This chapter builds the interface underneath that claim: a six-bit command and address bus, commands spread across one, two or four clock cycles, sampled on one clock edge only, and two channels on a single die that must be genuinely independent.

It also reports a pin count that most engineers — including me before I did the arithmetic — guess in the wrong direction. §4 works it out.

1. Six Pins Where LPDDR3 Had Ten

Start with the fact, because everything in this chapter follows from it.

DEVICE-VERIFIED, from a named LPDDR4 part's own description of its command interface and its own comparison against the previous generation:

LPDDR3LPDDR4
Command/address busCA0CA910 bitsCA[5:0]6 bits
Clock cycles per command11, 2 or 4
Sampled onboth clock edgespositive edge only

Three changes at once, and they are not independent — each is forced by the first.

The bus lost four bits. A DDR-style command has to convey an opcode, a bank, and a row or column address, and six bits cannot hold that. So something has to give.

What gives is time. A command that does not fit in one cycle is sent over two or four. The bus got narrower and commands got longer, which is the trade in one sentence.

And the sampling went single-edge. LPDDR3 sampled command bits on both clock edges, doubling the bits per clock from the same pins. LPDDR4 samples on the positive edge only.

That third change looks like a step backwards and is worth pausing on. Giving up double-edge sampling halves the bits per clock from a bus that had already lost four pins. §3 explains why a design would do that deliberately.

2. A Command Takes One, Two or Four Cycles

The duration is per command, not a mode. DEVICE-VERIFIED: the device's command truth table assigns each command a length, and the interface must handle all three.

The arithmetic of what each length carries is worth writing out. DERIVED from the pin count, the edge count and the cycle count, all device-verified; every product recomputed:

InterfacePinsEdgesCyclesBits per command
LPDDR3102120
LPDDR4, short6116
LPDDR4, medium61212
LPDDR4, long61424

Two observations that the table makes and the prose would not.

The long command carries more bits than LPDDR3's did — 24 against 20. So LPDDR4 did not reduce the information in a command; it reduced the rate at which that information can be delivered.

And the short command carries 6 bits where LPDDR3 delivered 20 in the same clock. For commands that genuinely need few bits, that is not a loss — a command needing 6 bits does not benefit from a bus that can carry 20.

So the three lengths exist because commands are not all the same size, and a fixed-length encoding would have to be as long as the longest. A four-cycle encoding for every command would waste three cycles on the ones that need one.

3. Why Give Up Double-Edge Sampling

§1 called this the change that looks backwards. The reason is that the two mechanisms — more pins and more edges — are not equally cheap at high speed.

A bit sampled on the negative edge has half a clock period of setup and hold available, not a full one. Both edges of a clock must be placed accurately relative to the data, and at the speeds this part reaches — DEVICE-VERIFIED 3200, 3733 and 4266 Mbps — the negative edge's placement is a harder problem than the positive edge's alone.

So the design decision reads as: spend cycles rather than edges. Four positive edges are four independent, well-defined sampling instants. Two edges of two clocks are the same count of instants but require the negative edge to be as trustworthy as the positive one.

Two supporting facts from the same datasheet make the picture coherent.

The device has no DLL. DEVICE-VERIFIED: the features list states “No DLL” outright.

No chapter in this curriculum owns the DLL in depth — 19.1 §5 names clock multiplication and phase generation as a PLL or DLL resource and classifies it as something that cannot be written in portable RTL, which is the extent of the coverage. So the one sentence needed here: a delay-locked loop holds an internal clock edge at a controlled phase relative to an external one, by a feedback loop that runs continuously whether or not the device is being accessed.

That last word is the whole reason to remove it. A loop that runs continuously burns power continuously, and on a device idling at the duty cycles 24.1 §3 measures, a continuous draw is paid for during the 98% of the time nothing is happening. A DLL is a good trade on a device that is usually busy and a poor one on a device that is usually idle — which is the LPDDR divergence 4.7 describes, appearing here as a single line in a features list.

And that removal costs edge placement precision. A device without a DLL has less ability to place an internal clock edge accurately relative to an external one — which makes a design that depends on the negative edge being as good as the positive edge considerably less attractive.

So single-edge command sampling and the absent DLL are the same decision seen from two sides. Neither datasheet statement says so, and this chapter does not claim the datasheet says so — but the two facts are consistent in a way worth naming, and §21's exercises ask you to test the reasoning rather than accept it.

4. Count the Pins for a x32 Interface

Here is the arithmetic that corrects the natural reading, and it is worth doing carefully because the intuitive answer is confidently wrong.

§1 says the CA bus went from 10 bits to 6. The natural conclusion is that LPDDR4 uses fewer command pins. For a x32 interface it uses more.

DEVICE-VERIFIED: the LPDDR4 part is organised as two independent x16 channels, each with its own CA[5:0], its own clock pair and its own CKE. A x32 LPDDR3 device is a single channel with one CA0CA9.

DERIVED, recomputed:

Interface at x32CA pinsCommand streams
LPDDR3, one x32 channel101
LPDDR4, two x16 channels2 × 6 = 122

LPDDR4 spends two extra CA pins to get a second independent command stream. That is the trade, and it is the opposite of the pin-saving story the per-channel numbers suggest.

Three consequences, and the third is the one that matters for a system.

The per-channel narrowing is real but it is not the point. Six bits per channel is what makes two channels affordable at all — at ten bits each, two channels would need twenty CA pins, which is double LPDDR3's and a much harder sell.

So the six-bit bus is an enabler, not a saving. It converts what would have been a 100% pin increase into a 20% one, and the 20% buys independence.

And independence is what 24.1 §6 argued mobile workloads need. That chapter established that a phone's memory traffic comes from many small, unrelated requesters rather than one large one. Two channels that can serve two requesters simultaneously address that directly, and one wide channel does not — which is why the two extra pins are worth spending.

5. Two Channels on One Die

DEVICE-VERIFIED, from the named 16Gb part: the die presents Channel A and Channel B, each x16, each with its own address space — the 16Gb device is 8Gb on Channel A and 8Gb on Channel B.

Each channel has its own:

  • CA[5:0] command and address bus
  • CK_t / CK_c clock pair
  • CKE clock enable
  • CS chip select
  • DQ[15:0] data bus with its strobes
  • DMI[1:0] data mask/inversion pins

The channels are not two halves of one interface. They do not share a command stream, a clock, or a power-down state. A controller can have Channel A in self refresh while Channel B serves a burst — which is why 24.2's state machine is instantiated per channel and not per device.

That last point deserves emphasis because it is where the architecture and the previous chapter meet. Every state in 24.2 §1's table is a per-channel state on this device. A device-level notion of “the memory is in self refresh” does not exist; each channel is somewhere, independently.

6. What Independence Actually Requires

“Independent” is easy to assert and specific to implement. As an invariant it has three parts, and §13's block checks all three.

No state crosses. Channel A's open row, its power-down state, its refresh progress and its mode registers are its own. A controller that tracks one row-open bit for the device has broken the invariant and will issue an illegal command on one channel based on the other's state.

No timing crosses. Channel A's tRCD, its refresh interval and its command-bus occupancy constrain Channel A. A controller that serialises the two channels' commands through one scheduler has not built two channels; it has built one channel with two data buses.

And the address spaces do not overlap. Each channel covers its own half. An address decoded to the wrong channel does not fail loudly — it reads a valid location holding unrelated data, which is the worst failure mode available.

7. What Is Shared Anyway

Independence is not total, and the exceptions are specific. DEVICE-VERIFIED: ZQ and RESET_n are shared between the two channels on the named part.

SignalScopeConsequence
CA[5:0], CK, CKE, CS, DQ, DMIper channelgenuine independence
RESET_nsharedresets both channels together
ZQsharedthe calibration reference serves both

Both exceptions have a controller consequence, and neither is cosmetic.

RESET_n cannot reset one channel. A controller that wants to reinitialise Channel A must reinitialise Channel B too, losing whatever state Channel B held. There is no per-channel recovery path, so an error on one channel that requires a reset costs the other channel's contents as well.

And ZQ is a shared resource that two channels may want at once. Chapter 22.1 owns what calibration is for; what matters here is that two independent channels contending for one reference need arbitration, and nothing in the interface provides it. The controller must serialise the two channels' calibration activity itself — which is the one place the “no timing crosses” rule of §6 has a genuine exception.

So independence is an invariant with two named exceptions, and a controller that treats it as absolute will eventually issue two calibrations at once or reset a channel it did not mean to touch.

8. Sixteen-n Prefetch

DEVICE-VERIFIED: the part uses a 16n prefetch architecture. Chapter 10.2 owns prefetch as a general mechanism; what this section adds is the granularity it forces on this specific interface.

DERIVED, and the arithmetic is short:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   16n prefetch on a x16 channel
   = 16 transfers x 16 bits
   = 256 bits
   = 32 bytes minimum per channel burst

Thirty-two bytes is the smallest thing this channel can move. Not the smallest efficient thing — the smallest thing. A one-byte read moves 32 bytes.

That number is what makes 24.1 §2's claim computable at last. That chapter argued that a wide channel worsens energy per bit for small accesses and recorded the effect as excluded from its accountant — terms_excluded bit 4, “wasted payload on small accesses,” permanently set because nothing in that chapter could compute it. With a verified granularity, it can be computed. DERIVED, every row recomputed:

RequestBurstsBytes movedWastedUseful
4 B1322812.5%
8 B1322425.0%
16 B1321650.0%
32 B1320100%
64 B2640100%

A four-byte access wastes seven eighths of the energy it spends. That is the quantity 24.1 named and could not measure, and §14's block computes it.

9. Why Two Narrow Channels Beat One Wide One, Numerically

§8's table plus §5's organisation produce the argument 4.7 §5 makes qualitatively.

Consider a x32 interface serving requests. DERIVED:

OrganisationMinimum granuleRequesters served at once
One x32 channel64 B1
Two x16 channels, one access spanning both64 B1
Two x16 channels, serving separately32 B each2

The third row is the whole case. Two independent channels can either behave like one wide channel — 64 bytes, one requester — or serve two requesters at 32 bytes each. One wide channel has only the first option.

So for 24.1 §6's many-small-requesters workload, two narrow channels halve the granule and double the concurrency simultaneously. For a single large sequential stream they are equivalent to one wide channel. The organisation is never worse and is sometimes much better, which is why the two extra CA pins of §4 are a good trade rather than a compromise.

10. The Architecture as a Stack

The LPDDR4 interface as five layers, top to bottom. At the top the controller holds two independent schedulers, one per channel, because no state and no timing may cross between them. Below it the per-channel command stream carries a six-bit command and address bus with its own clock and clock enable. Below that the multi-cycle command encoding spreads a command across one, two or four positive clock edges, which sets a command-rate ceiling of one quarter for the longest commands. Below that the sixteen-n prefetch fixes the minimum transfer at thirty-two bytes on a sixteen-bit channel, which is what makes a small access wasteful. At the bottom the shared device resources, reset and the calibration reference, are the two exceptions to channel independence.LPDDR4: two independent channels over a narrow multi-cycle commandinterfaceController — two independent schedulersone per channel; no state and no timing may cross (§6)one per channel; no state and no timing may cross (§6)Per-channel command stream — CA[5:0], CK, CKE, CSeach channel has its own; 12 CA pins for a x32 interface, not 10 (§4)each channel has its own; 12 CA pins for a x32 interface, not 10 (§4)Multi-cycle encoding — 1, 2 or 4 positive edges6, 12 or 24 bits per command; a 0.25 commands-per-clock ceiling on the longest (§2)6, 12 or 24 bits per command; a 0.25 commands-per-clock ceiling on the longest (§2)16n prefetch — 32 B minimum per x16 channel bursta 4 B request moves 32 B and wastes 87.5% of it (§8)a 4 B request moves 32 B and wastes 87.5% of it (§8)Shared device resources — RESET_n, ZQthe two named exceptions to independence; neither can be per channel (§7)the two named exceptions to independence; neither can be per channel (§7)

Two things the stack makes visible.

Independence lives at the top and is broken only at the bottom. Four of the five layers are per channel. The exceptions are in the bottom layer, and they are exactly the two signals §7 names — which is why a controller can treat independence as near-absolute and must handle two specific cases.

And the two constraints that limit performance sit in the middle two layers. The command-rate ceiling comes from the encoding; the granularity floor comes from the prefetch. Neither is a power-saving mechanism — they are the costs paid for the narrow bus and the wide internal array, and 24.2's savings mechanisms are nowhere in this diagram because they are a different concern entirely.

11. The CA Decoder

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ---------------------------------------------------------------------
// lp4_ca_decoder -- assembles a command from 1, 2 or 4 consecutive
// positive-edge samples of a 6-bit CA bus.
//
// CLASSIFICATION: educational, synthesisable.
//
// VERIFIED STRUCTURE, ILLUSTRATIVE ENCODING. That commands span 1, 2
// or 4 cycles on a 6-bit bus sampled on the positive edge only is
// DEVICE-VERIFIED (§1). The specific opcode-to-length mapping below
// is ILLUSTRATIVE and is NOT reproduced from any datasheet's truth
// table -- §11's header says so and it matters.
//
// WHAT IT DOES NOT MODEL:
//   - the device, the electrical interface, or any timing parameter
//   - the real command set (Module 7 owns DDR's; this is not it)
//   - what the device does with a malformed command, which is not
//     defined to be a clean rejection
//
// WHY THE OCCUPANCY OUTPUT EXISTS: §2's callout. A 4-cycle command
// holds the bus for 4 clocks and cannot be interleaved, so the bus is
// a schedulable resource and a controller needs to see it.
// ---------------------------------------------------------------------
module lp4_ca_decoder #(
  parameter int CA_W    = 6,        // DEVICE-VERIFIED width
  parameter int MAX_CY  = 4,        // DEVICE-VERIFIED maximum length
  parameter int CMD_W   = CA_W * MAX_CY,
  parameter int CY_W    = $clog2(MAX_CY + 1),   // a COUNT, not an index
  parameter int CNT_W   = 20
) (
  input  logic              clk,
  input  logic              rst_n,

  // ── The interface. Sampled on the POSITIVE EDGE ONLY -- there is no
  //    negative-edge path in this block, deliberately (§1, §3).
  input  logic              cke,
  input  logic              cs,
  input  logic [CA_W-1:0]   ca,

  // ── Assembled output.
  output logic              cmd_valid,
  output logic [CMD_W-1:0]  cmd_bits,
  output logic [CY_W-1:0]   cmd_length,

  // ── Bus occupancy. §2's callout: the command bus is a resource.
  output logic              bus_busy,
  output logic [CY_W-1:0]   cycles_remaining,
  output logic [CNT_W-1:0]  cy_bus_occupied,
  output logic [CNT_W-1:0]  cnt_cmds_1cy,
  output logic [CNT_W-1:0]  cnt_cmds_2cy,
  output logic [CNT_W-1:0]  cnt_cmds_4cy,

  // ── Malformed assembly, reported by kind.
  output logic              err_truncated,     // CS dropped mid-command
  output logic              err_cke_low_mid,   // CKE dropped mid-command
  output logic              err_new_cmd_mid,   // a start arrived mid-command
  output logic              err_unknown_length,
  output logic              any_malformed
);

  // ILLUSTRATIVE length mapping, from the top two bits of the first
  // sample. NOT a datasheet truth table (§11's header).
  localparam logic [1:0] L_ONE  = 2'b00;
  localparam logic [1:0] L_TWO  = 2'b01;
  localparam logic [1:0] L_FOUR = 2'b10;

  initial begin
    if (CA_W != 6)
      // Not a functional requirement -- a warning that the block is
      // being used outside the width §1 verified.
      $display("lp4_ca_decoder: NOTE -- CA_W=%0d, device-verified width is 6",
               CA_W);
    if (MAX_CY != 4)
      $fatal(1, "lp4_ca_decoder: MAX_CY must be 4 (device-verified)");
    if (CMD_W != CA_W * MAX_CY)
      $fatal(1, "lp4_ca_decoder: CMD_W must be CA_W*MAX_CY");
  end

  logic [CMD_W-1:0] acc;
  logic [CY_W-1:0]  need, got;
  logic             active;
  logic [CNT_W-1:0] occ, n1, n2, n4;
  logic             v, e_tr, e_ck, e_nw, e_ul;
  logic [CY_W-1:0]  out_len;
  logic [CMD_W-1:0] out_bits;

  // A command STARTS when CS is asserted with CKE high. Declared above
  // the continuous assigns that read it.
  logic start;
  assign start = cke && cs;

  // Length of a starting command, from its first sample.
  logic [CY_W-1:0] len_of_start;
  logic            len_known;
  always_comb begin
    unique case (ca[CA_W-1 -: 2])
      L_ONE   : begin len_of_start = CY_W'(1); len_known = 1'b1; end
      L_TWO   : begin len_of_start = CY_W'(2); len_known = 1'b1; end
      L_FOUR  : begin len_of_start = CY_W'(4); len_known = 1'b1; end
      default : begin len_of_start = CY_W'(1); len_known = 1'b0; end
    endcase
  end

  always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
      acc <= '0; need <= '0; got <= '0; active <= 1'b0;
      occ <= '0; n1 <= '0; n2 <= '0; n4 <= '0;
      v <= 1'b0; out_len <= '0; out_bits <= '0;
      e_tr <= 1'b0; e_ck <= 1'b0; e_nw <= 1'b0; e_ul <= 1'b0;
    end else begin
      v <= 1'b0;
      e_tr <= 1'b0; e_ck <= 1'b0; e_nw <= 1'b0; e_ul <= 1'b0;

      if (active) begin
        if (occ != {CNT_W{1'b1}}) occ <= occ + CNT_W'(1);

        // ── Mid-command hazards, each reported separately so a debug
        //    session learns WHICH way the assembly broke.
        if (!cke) begin
          // §1: CKE low mid-command. The remaining samples will not
          // arrive, so the partial command is abandoned rather than
          // completed with stale bits -- completing it would
          // fabricate a command the controller never sent.
          e_ck   <= 1'b1;
          active <= 1'b0;
          acc    <= '0;
          got    <= '0;
        end else if (start && (got != need)) begin
          // A new command asserted while one is still assembling. The
          // bus cannot carry two, so the new one is REJECTED and the
          // in-flight one continues. Dropping the in-flight one
          // instead would lose a command the controller believes it
          // issued.
          e_nw <= 1'b1;
          acc[got*CA_W +: CA_W] <= ca;
          got <= got + CY_W'(1);
          if ((got + CY_W'(1)) == need) begin
            v <= 1'b1; out_bits <= acc; out_len <= need; active <= 1'b0;
          end
        end else if (!cs && (got != need)) begin
          // §16's truncation row: the strobe went away before the
          // command finished.
          e_tr   <= 1'b1;
          active <= 1'b0;
          acc    <= '0;
          got    <= '0;
        end else begin
          acc[got*CA_W +: CA_W] <= ca;
          if ((got + CY_W'(1)) == need) begin
            v        <= 1'b1;
            // The final sample is folded in COMBINATIONALLY for the
            // output, not read back from acc -- acc's last write lands
            // on this same edge, so reading it would publish the
            // PRE-fold value. This is the stale-register hazard
            // Module 21 §10 names.
            out_bits <= acc | (CMD_W'(ca) << (got*CA_W));
            out_len  <= need;
            active   <= 1'b0;
            acc      <= '0;
            got      <= '0;
            unique case (need)
              CY_W'(1) : if (n1 != {CNT_W{1'b1}}) n1 <= n1 + CNT_W'(1);
              CY_W'(2) : if (n2 != {CNT_W{1'b1}}) n2 <= n2 + CNT_W'(1);
              CY_W'(4) : if (n4 != {CNT_W{1'b1}}) n4 <= n4 + CNT_W'(1);
              default  : ;
            endcase
          end else begin
            got <= got + CY_W'(1);
          end
        end
      end else if (start) begin
        if (!len_known) begin
          e_ul <= 1'b1;
        end else if (len_of_start == CY_W'(1)) begin
          // A one-cycle command completes on its own start edge and
          // never occupies the bus beyond it.
          v        <= 1'b1;
          out_bits <= CMD_W'(ca);
          out_len  <= CY_W'(1);
          if (occ != {CNT_W{1'b1}}) occ <= occ + CNT_W'(1);
          if (n1 != {CNT_W{1'b1}}) n1 <= n1 + CNT_W'(1);
        end else begin
          active <= 1'b1;
          need   <= len_of_start;
          got    <= CY_W'(1);
          acc    <= CMD_W'(ca);
          if (occ != {CNT_W{1'b1}}) occ <= occ + CNT_W'(1);
        end
      end
    end
  end

  assign cmd_valid        = v;
  assign cmd_bits         = out_bits;
  assign cmd_length       = out_len;
  assign bus_busy         = active;
  assign cycles_remaining = active ? (need - got) : CY_W'(0);
  assign cy_bus_occupied  = occ;
  assign cnt_cmds_1cy     = n1;
  assign cnt_cmds_2cy     = n2;
  assign cnt_cmds_4cy     = n4;
  assign err_truncated      = e_tr;
  assign err_cke_low_mid    = e_ck;
  assign err_new_cmd_mid    = e_nw;
  assign err_unknown_length = e_ul;
  assign any_malformed = e_tr | e_ck | e_nw | e_ul;
endmodule

12. The Hazard That Multi-Cycle Assembly Creates

One line of §11 deserves its own section, because it is the same defect class Module 21 §10 names and it appears here for a structurally different reason.

The final sample of a multi-cycle command is written into acc on the same clock edge that publishes the completed command. Reading acc on that edge would publish the value from before the final write. So the output folds the last sample in combinationally:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
out_bits <= acc | (CMD_W'(ca) << (got*CA_W));

rather than

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
out_bits <= acc;              // WRONG -- one sample short

The symptom would be subtle and consistent: every multi-cycle command missing its last six bits, with one-cycle commands entirely correct. A decoder with this bug decodes short commands perfectly and long ones wrongly, which points a debugging session at the command set rather than at the assembly.

Module 21 §10 found the same hazard in a search engine, where a region was folded into a best-width register in the sequential block of the deciding state. The two cases share nothing but the shape: a value is read in the same cycle a sequential write to it is landing. That shape is worth recognising on sight, because in both cases every assertion about the mechanism passes and only the value is wrong.

13. The Channel Independence Checker

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ---------------------------------------------------------------------
// lp4_channel_guard -- the §6 independence invariant as a checker,
// plus arbitration for the §7 shared resources.
//
// CLASSIFICATION: educational, synthesisable.
//
// WHAT IT DOES NOT MODEL:
//   - either channel's internal state. 24.2's lp_state_machine is
//     instantiated PER CHANNEL (§5) and this block sits beside two of
//     them rather than containing them.
//   - a legal address sent to the WRONG channel. §6's callout: that
//     is indistinguishable from a correct access at this level, and a
//     block claiming to catch it would be claiming the impossible.
//     What IS checkable is that selection depends only on the bits
//     assigned to it, which is the invariant that makes the bug
//     possible in the first place.
//
// WHY ARBITRATION IS HERE AND NOT PER CHANNEL: §7. RESET_n and ZQ are
// shared, so they are the ONE place where two independent channels
// must be serialised, and a per-channel block cannot do it.
// ---------------------------------------------------------------------
module lp4_channel_guard #(
  parameter int ADDR_W  = 34,
  // Address bit selecting the channel. On the named part the die is
  // split evenly between two channels (§5), so exactly one bit does
  // the selecting -- and WHICH bit is a controller mapping choice
  // with the §9 consequence.
  parameter int SEL_BIT = 33,
  parameter int CNT_W   = 20
) (
  input  logic              clk,
  input  logic              rst_n,

  // ── Per-channel activity. Two of everything, never one.
  input  logic              a_cmd_valid,
  input  logic              b_cmd_valid,
  input  logic [ADDR_W-1:0] a_addr,
  input  logic [ADDR_W-1:0] b_addr,
  input  logic              a_bus_busy,
  input  logic              b_bus_busy,

  // ── Shared-resource requests (§7).
  input  logic              a_zq_req,
  input  logic              b_zq_req,
  input  logic              reset_req,

  // ── Arbitrated shared-resource grants.
  output logic              a_zq_grant,
  output logic              b_zq_grant,
  output logic              reset_both,

  // ── Independence observation. Concurrency is the PROPERTY, so it
  //    is counted rather than merely permitted.
  output logic [CNT_W-1:0]  cy_both_busy,
  output logic [CNT_W-1:0]  cy_only_a,
  output logic [CNT_W-1:0]  cy_only_b,
  output logic [CNT_W-1:0]  cy_neither,
  output logic              concurrency_observed,

  // ── Violations.
  output logic              err_wrong_channel_a,
  output logic              err_wrong_channel_b,
  output logic              err_zq_both,
  output logic              err_reset_one_channel,
  output logic              any_violation
);

  initial begin
    if (SEL_BIT >= ADDR_W)
      $fatal(1, "lp4_channel_guard: SEL_BIT (%0d) outside ADDR_W (%0d)",
             SEL_BIT, ADDR_W);
  end

  logic [CNT_W-1:0] c_both, c_a, c_b, c_none;
  logic             e_wa, e_wb, e_zq, e_r1, saw_conc;
  logic             zq_last_a;

  // Declared above the assigns that read them.
  logic a_sel_ok, b_sel_ok;
  assign a_sel_ok = (a_addr[SEL_BIT] == 1'b0);
  assign b_sel_ok = (b_addr[SEL_BIT] == 1'b1);

  always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
      c_both <= '0; c_a <= '0; c_b <= '0; c_none <= '0;
      e_wa <= 1'b0; e_wb <= 1'b0; e_zq <= 1'b0; e_r1 <= 1'b0;
      saw_conc <= 1'b0; zq_last_a <= 1'b0;
    end else begin
      e_wa <= 1'b0; e_wb <= 1'b0; e_zq <= 1'b0; e_r1 <= 1'b0;

      // ── Occupancy, four-way and exhaustive. §6: a controller that
      //    never reaches "both busy" has not built two channels.
      case ({a_bus_busy, b_bus_busy})
        2'b11 : begin
          if (c_both != {CNT_W{1'b1}}) c_both <= c_both + CNT_W'(1);
          saw_conc <= 1'b1;
        end
        2'b10 : if (c_a    != {CNT_W{1'b1}}) c_a    <= c_a    + CNT_W'(1);
        2'b01 : if (c_b    != {CNT_W{1'b1}}) c_b    <= c_b    + CNT_W'(1);
        2'b00 : if (c_none != {CNT_W{1'b1}}) c_none <= c_none + CNT_W'(1);
      endcase

      // ── The checkable half of §6's third rule: selection must be a
      //    function of the assigned bit and nothing else.
      if (a_cmd_valid && !a_sel_ok) e_wa <= 1'b1;
      if (b_cmd_valid && !b_sel_ok) e_wb <= 1'b1;

      // ── §7: ZQ is shared. Simultaneous requests are a real error
      //    and not merely arbitrated away silently -- a controller
      //    that issues both has not serialised its calibration.
      if (a_zq_req && b_zq_req) e_zq <= 1'b1;
      if (a_zq_req ^ b_zq_req)  zq_last_a <= a_zq_req;
    end
  end

  // Alternating priority, so neither channel starves the other when
  // both ask repeatedly.
  assign a_zq_grant = a_zq_req && (!b_zq_req || zq_last_a == 1'b0);
  assign b_zq_grant = b_zq_req && !a_zq_grant;
  // §7: RESET_n is shared, so it is a DEVICE action. There is no
  // per-channel reset, and the output name says so.
  assign reset_both  = reset_req;
  assign err_reset_one_channel = 1'b0;   // structurally impossible here

  assign cy_both_busy = c_both;
  assign cy_only_a    = c_a;
  assign cy_only_b    = c_b;
  assign cy_neither   = c_none;
  assign concurrency_observed = saw_conc;
  assign err_wrong_channel_a = e_wa;
  assign err_wrong_channel_b = e_wb;
  assign err_zq_both         = e_zq;
  assign any_violation = e_wa | e_wb | e_zq;
endmodule

reset_both is named for what it does. §7 established there is no per-channel reset on this part, so an output called a_reset would invite a controller to believe one exists. err_reset_one_channel is tied low with a comment saying why: the violation is structurally unreachable because the interface offers no way to attempt it.

And cy_both_busy exists because concurrency is the property, not a side effect. A controller can satisfy every independence rule and still serialise the two channels through one scheduler, wasting the architecture entirely. The four-way occupancy count makes that visible — §16's DV table drives it.

14. The Prefetch Granularity Accountant

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ---------------------------------------------------------------------
// lp4_prefetch_accountant -- bytes moved versus bytes requested,
// against the DEVICE-VERIFIED 16n prefetch granularity of §8.
//
// CLASSIFICATION: educational, synthesisable.
//
// WHAT IT CLOSES: 24.1 §11's terms_excluded bit 4, "wasted payload on
// small accesses", was permanently set because nothing in that
// chapter could compute it. A verified granularity makes it
// computable, and this block computes it.
//
// WHAT IT DOES NOT MODEL: energy. Waste is reported in BYTES, not in
// joules, because converting requires a per-bit energy figure no
// datasheet consulted in this module publishes. §14's header says so.
// ---------------------------------------------------------------------
module lp4_prefetch_accountant #(
  // DEVICE-VERIFIED for a x16 channel: 16 transfers x 16 bits = 32 B.
  parameter int GRAN_B  = 32,
  parameter int REQ_W   = 16,
  parameter int ACC_W   = 40,
  parameter int GRAN_SH = $clog2(GRAN_B)
) (
  input  logic              clk,
  input  logic              rst_n,

  input  logic              req_valid,
  input  logic [REQ_W-1:0]  req_bytes,

  // ── Totals. Reported as a PAIR, never as a ratio -- the caller
  //    states its denominator, the discipline every accountant in
  //    this module follows.
  output logic [ACC_W-1:0]  bytes_requested,
  output logic [ACC_W-1:0]  bytes_moved,
  output logic [ACC_W-1:0]  bytes_wasted,
  output logic [ACC_W-1:0]  bursts_issued,

  // ── Per-request, for the current request only.
  output logic [REQ_W+1:0]  this_moved,
  output logic [REQ_W+1:0]  this_wasted,
  output logic              this_is_partial,

  // Small-access classification. §8: a sub-granule request wastes
  // most of what it spends, and the count is the signal a system
  // needs in order to consider coalescing.
  output logic [ACC_W-1:0]  cnt_sub_granule,
  output logic [ACC_W-1:0]  cnt_exact,

  output logic              err_zero_length_req,
  output logic              any_saturated
);

  initial begin
    if (GRAN_B < 2 || (GRAN_B & (GRAN_B - 1)) != 0)
      // A non-power-of-two granule would need a divide to round up,
      // and no real prefetch granularity is one.
      $fatal(1, "lp4_prefetch_accountant: GRAN_B must be a power of two >= 2");
    if (ACC_W <= REQ_W)
      $fatal(1, "lp4_prefetch_accountant: ACC_W must exceed REQ_W");
  end

  logic [ACC_W-1:0] t_req, t_mov, t_bur, n_sub, n_exact;
  logic             e_zero, sat;

  // Round up to the granule by shifting -- no divide. Declared above
  // the assigns that read them.
  logic [REQ_W+1:0] nburst, moved;
  assign nburst = (req_bytes == '0) ? '0
                : (REQ_W+2)'(((req_bytes - REQ_W'(1)) >> GRAN_SH) + 1);
  assign moved  = nburst << GRAN_SH;

  always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
      t_req <= '0; t_mov <= '0; t_bur <= '0;
      n_sub <= '0; n_exact <= '0;
      e_zero <= 1'b0; sat <= 1'b0;
    end else begin
      e_zero <= 1'b0;
      if (req_valid) begin
        if (req_bytes == '0) begin
          e_zero <= 1'b1;
        end else begin
          if (t_req > ({ACC_W{1'b1}} - ACC_W'(moved))) sat <= 1'b1;
          else begin
            t_req <= t_req + ACC_W'(req_bytes);
            t_mov <= t_mov + ACC_W'(moved);
            t_bur <= t_bur + ACC_W'(nburst);
          end
          if (req_bytes < REQ_W'(GRAN_B)) begin
            if (n_sub != {ACC_W{1'b1}}) n_sub <= n_sub + ACC_W'(1);
          end else if (moved == (REQ_W+2)'(req_bytes)) begin
            if (n_exact != {ACC_W{1'b1}}) n_exact <= n_exact + ACC_W'(1);
          end
        end
      end
    end
  end

  assign bytes_requested = t_req;
  assign bytes_moved     = t_mov;
  // Moved is always >= requested by construction, so the subtraction
  // cannot go negative -- P11 asserts the invariant that guarantees it.
  assign bytes_wasted    = (t_mov >= t_req) ? (t_mov - t_req) : '0;
  assign bursts_issued   = t_bur;
  assign this_moved      = moved;
  assign this_wasted     = moved - (REQ_W+2)'(req_bytes);
  assign this_is_partial = req_valid && (req_bytes < REQ_W'(GRAN_B));
  assign cnt_sub_granule = n_sub;
  assign cnt_exact       = n_exact;
  assign err_zero_length_req = e_zero;
  assign any_saturated   = sat;
endmodule

15. What the Assertions Prove

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// Bind unit note: P1-P7 reference lp4_ca_decoder's internals, P8-P10
// lp4_channel_guard's, P11-P14 lp4_prefetch_accountant's. Each is
// written as though bound into its module.

// P1 -- a command is never longer than the device-verified maximum.
property p_length_bounded;
  @(posedge clk) disable iff (!rst_n)
    cmd_valid |-> (cmd_length inside {CY_W'(1), CY_W'(2), CY_W'(4)});
endproperty
assert property (p_length_bounded);

// P2 -- the bus cannot carry two commands. While one is assembling,
// no second command completes. §2: the bus is a serial resource.
property p_bus_is_exclusive;
  @(posedge clk) disable iff (!rst_n)
    (bus_busy && (cycles_remaining > CY_W'(1))) |-> !cmd_valid;
endproperty
assert property (p_bus_is_exclusive);

// P3 -- the countdown is monotone while a command assembles. Rules
// out a restart that would silently lengthen the command.
property p_countdown_monotone;
  @(posedge clk) disable iff (!rst_n)
    (bus_busy && $past(bus_busy, 1) && !any_malformed)
      |-> (cycles_remaining < $past(cycles_remaining, 1));
endproperty
assert property (p_countdown_monotone);

// P4 -- a truncated or CKE-interrupted command is NEVER published.
// §11: completing it with stale bits would fabricate a command the
// controller never issued, which is worse than losing one.
property p_malformed_never_published;
  @(posedge clk) disable iff (!rst_n)
    (err_truncated || err_cke_low_mid) |-> !cmd_valid;
endproperty
assert property (p_malformed_never_published);

// P5 -- a one-cycle command never occupies the bus past its own edge.
property p_one_cycle_does_not_occupy;
  @(posedge clk) disable iff (!rst_n)
    (cmd_valid && (cmd_length == CY_W'(1))) |-> !bus_busy;
endproperty
assert property (p_one_cycle_does_not_occupy);

// P6 -- the per-length counters partition the completed commands.
// A command that completed is counted exactly once.
property p_length_counters_partition;
  @(posedge clk) disable iff (!rst_n)
    (cmd_valid && (cmd_length == CY_W'(2)))
      |=> (cnt_cmds_2cy == $past(cnt_cmds_2cy, 1) + CNT_W'(1));
endproperty
assert property (p_length_counters_partition);

// P7 -- §12's hazard, as a property. The published command must carry
// the FINAL sample, which the stale-register bug would omit. Checked
// by requiring the top field of a 4-cycle command to be non-zero
// whenever the final sample was.
property p_final_sample_included;
  @(posedge clk) disable iff (!rst_n)
    (cmd_valid && (cmd_length == CY_W'(4)) && ($past(ca, 1) != '0))
      |-> (cmd_bits[CMD_W-1 -: CA_W] != '0);
endproperty
assert property (p_final_sample_included);

// P8 -- CHANNEL INDEPENDENCE. Neither channel's command validity
// constrains the other's. Asserted as the ABSENCE of an implication:
// both busy simultaneously must be reachable, which is what the
// concurrency cover checks -- and no violation may be raised for it.
property p_concurrency_is_legal;
  @(posedge clk) disable iff (!rst_n)
    (a_bus_busy && b_bus_busy) |-> !any_violation;
endproperty
assert property (p_concurrency_is_legal);

// P9 -- the four occupancy counters partition every cycle exactly.
property p_occupancy_partitions_cycles;
  @(posedge clk) disable iff (!rst_n)
    ((cy_both_busy + cy_only_a + cy_only_b + cy_neither) ==
     ($past(cy_both_busy,1) + $past(cy_only_a,1) +
      $past(cy_only_b,1)   + $past(cy_neither,1) + CNT_W'(1)));
endproperty
assert property (p_occupancy_partitions_cycles);

// P10 -- §7: ZQ is granted to at most one channel. The shared
// resource is genuinely serialised.
property p_zq_mutually_exclusive;
  @(posedge clk) disable iff (!rst_n) !(a_zq_grant && b_zq_grant);
endproperty
assert property (p_zq_mutually_exclusive);

// P11 -- bytes moved is never less than bytes requested. The
// invariant that makes §14's subtraction safe: a granule-rounded
// transfer can only be larger.
property p_moved_at_least_requested;
  @(posedge clk) disable iff (!rst_n) (bytes_moved >= bytes_requested);
endproperty
assert property (p_moved_at_least_requested);

// P12 -- bytes moved is always a whole number of granules. §8: 32 B
// is the smallest thing the channel can move, not the smallest
// efficient thing.
property p_moved_is_whole_granules;
  @(posedge clk) disable iff (!rst_n)
    ((bytes_moved & ACC_W'(GRAN_B - 1)) == '0);
endproperty
assert property (p_moved_is_whole_granules);

// P13 -- a sub-granule request always moves exactly one granule, and
// never zero. The 12.5%-useful row of §8's table, as a property.
property p_sub_granule_moves_one;
  @(posedge clk) disable iff (!rst_n)
    (req_valid && (req_bytes != '0) && (req_bytes < REQ_W'(GRAN_B)))
      |-> (this_moved == (REQ_W+2)'(GRAN_B));
endproperty
assert property (p_sub_granule_moves_one);

// P14 -- a zero-length request moves nothing and is reported. It is
// not silently rounded up to a granule, which would charge the
// system for a transfer it never asked for.
property p_zero_length_moves_nothing;
  @(posedge clk) disable iff (!rst_n)
    (req_valid && (req_bytes == '0)) |=> err_zero_length_req;
endproperty
assert property (p_zero_length_moves_nothing);

// ── Cover.
// All three command lengths actually assembled.
cover property (@(posedge clk) disable iff (!rst_n)
                  cmd_valid && (cmd_length == CY_W'(1)));
cover property (@(posedge clk) disable iff (!rst_n)
                  cmd_valid && (cmd_length == CY_W'(2)));
cover property (@(posedge clk) disable iff (!rst_n)
                  cmd_valid && (cmd_length == CY_W'(4)));
// Each malformed kind reached. A property forbidding something is
// vacuous until the testbench attempts it.
cover property (@(posedge clk) disable iff (!rst_n) err_truncated);
cover property (@(posedge clk) disable iff (!rst_n) err_cke_low_mid);
cover property (@(posedge clk) disable iff (!rst_n) err_new_cmd_mid);
cover property (@(posedge clk) disable iff (!rst_n) err_unknown_length);
// Back-to-back 4-cycle commands -- the 0.25 commands-per-clock
// ceiling of §2's callout, actually exercised.
cover property (@(posedge clk) disable iff (!rst_n)
                  cmd_valid && (cmd_length == CY_W'(4))
                  ##1 bus_busy [*3] ##1 cmd_valid);
// THE INDEPENDENCE COVER. Both channels busy at once. A suite that
// never hits this has not tested the architecture's whole purpose.
cover property (@(posedge clk) disable iff (!rst_n)
                  a_bus_busy && b_bus_busy);
// Both channels contending for ZQ -- §7's one genuine exception.
cover property (@(posedge clk) disable iff (!rst_n) err_zq_both);
// Each row of §8's table: sub-granule, exact, multi-granule.
cover property (@(posedge clk) disable iff (!rst_n) this_is_partial);
cover property (@(posedge clk) disable iff (!rst_n)
                  req_valid && (this_wasted == '0));
cover property (@(posedge clk) disable iff (!rst_n)
                  req_valid && (this_moved > (REQ_W+2)'(GRAN_B)));

16. Corner Cases

CaseBehaviourWhy
One-cycle commandCompletes on its start edge; bus never busyP5 — it cannot be interleaved with anything
CS deasserted mid-commanderr_truncated; nothing publishedP4 — a fabricated command is worse than a lost one
CKE low mid-commanderr_cke_low_mid; assembly abandonedThe remaining samples will not arrive
New command asserted mid-commanderr_new_cmd_mid; in-flight command continuesThe bus carries one; dropping the in-flight one loses a real command
Unknown length fielderr_unknown_length; nothing assembledGuessing a length would mis-frame every following command
Final sample folded from accEvery multi-cycle command short six bits§12's hazard; P7 catches it
Back-to-back four-cycle commands0.25 commands per clock§2's ceiling — a real scheduling limit
Both channels busy simultaneouslyLegal and countedP8 — the architecture's purpose
Both channels request ZQerr_zq_both; one grant only§7 — the shared resource needs serialising
Reset requestedreset_both — both channels§7 — no per-channel reset exists
Legal address on the wrong channelUndetectable here; valid data returned§6's callout — the silent failure
Request of 1 byteOne 32 B burst; 31 B wasted§8 — 32 B is the minimum, not the minimum efficient
Request of exactly 32 BOne burst, zero wasteThe granule boundary
Request of 33 BTwo bursts, 31 B wastedCrossing a granule costs a whole granule
Zero-length requesterr_zero_length_req; nothing movedP14 — not silently rounded to a granule
GRAN_B not a power of two$fatal at elaborationRounding up would need a divide; no real granularity is one

Row eleven is the honest limit of the chapter, and it is in the table rather than buried because a reader deserves to know which failure the tooling cannot see.

Row fourteen is worth noticing separately. A 33-byte request wastes 31 bytes, one more byte requested than the granule and a whole extra granule moved. That is the sharpest form of §8's argument: waste is not proportional to how much you over-ask by, it is quantised.

17. DV

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// Independent CA reference. Assembles commands from a QUEUE of
// samples rather than from a state machine, so agreement between the
// two is evidence rather than a restatement of one implementation.
// SIMULATION-ONLY.
class ca_assembly_ref;
  bit [5:0] q[$];
  int       need;
  int       n_published;

  function void reset();
    q.delete(); need = 0; n_published = 0;
  endfunction

  function int length_of(bit [5:0] first);
    case (first[5:4])
      2'b00  : return 1;
      2'b01  : return 2;
      2'b10  : return 4;
      default: return -1;          // unknown
    endcase
  endfunction

  // Returns the assembled width in bits when a command completes,
  // 0 while assembling, -1 on a malformed sequence.
  function int sample(bit cke, bit cs, bit [5:0] ca);
    if (!cke && q.size() > 0) begin q.delete(); need = 0; return -1; end
    if (q.size() == 0) begin
      if (!(cke && cs)) return 0;
      need = length_of(ca);
      if (need < 0) return -1;
      q.push_back(ca);
    end else begin
      if (!cs) begin q.delete(); need = 0; return -1; end
      q.push_back(ca);
    end
    if (q.size() == need) begin
      int w = q.size() * 6;
      q.delete(); need = 0; n_published++;
      return w;
    end
    return 0;
  endfunction
endclass
CheckWhat it establishes
Every command the DUT publishes matches the reference's assemblyTwo representations agree
Publish counts match over a long random streamNo command silently lost or duplicated
Assemble all three lengths; compare bit-for-bitThe three length covers, and P7 on the four-cycle case
Truncate at each cycle position of a four-cycle commanderr_truncated at every position, nothing published
Drop CKE at each cycle positionerr_cke_low_mid at every position
Assert a new command at each mid-command positionerr_new_cmd_mid; the in-flight command still completes
Drive the unknown length encodingerr_unknown_length; the following command frames correctly
Back-to-back four-cycle stream; measure commands per clockThe 0.25 ceiling, measured rather than asserted
Drive both channels simultaneously; confirm no violationP8 and the independence cover
Serialise both channels through one scheduler; observe cy_both_busyThe failure a passing assertion suite hides
Both channels request ZQ on the same cycle, repeatedlyP10, and that neither channel starves
Sweep request size 1…128 B; compare waste against a reference§8's table, every row
Request exactly 32 B and exactly 33 BThe quantisation of §16's row fourteen

The tenth check produces the report that matters most, because it is a failure no assertion reports:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  THE SERIALISED CONTROLLER THAT PASSES EVERY ASSERTION
    Two independent channels, one scheduler issuing to them in turn.

    assertions: P8  concurrency is legal        PASS
                P9  occupancy partitions        PASS
                P10 ZQ mutually exclusive       PASS
                all 14 properties               PASS

    coverage  : a_bus_busy && b_bus_busy        NEVER HIT

    occupancy : cy_both_busy      0
                cy_only_a    41,208
                cy_only_b    40,976
                cy_neither   17,816

    diagnosis : the design is CORRECT and the architecture is WASTED.
      Both channels work, neither ever works at the same time as the
      other, and the two extra CA pins of §4 bought nothing. Peak
      concurrency is 1 where the device offers 2.

    what makes it hard to catch : there is no error. Nothing is
      illegal, no property is violated, and every transaction
      completes. The only evidence is a zero in an occupancy counter
      and an unhit cover.

    the fix : cy_both_busy is a REQUIRED coverage goal, not a
      diagnostic. An LPDDR4 controller whose both-busy count is zero
      has built one channel with two data buses (§6).

18. Debugging

SymptomLikely causeHow to confirm
Short commands decode, long ones do not§12's stale-register foldcmd_bits top field zero on four-cycle commands; P7
Every command after a certain point is wrongA length mis-framed, shifting all subsequent framingerr_unknown_length; check the first bad command, not the symptoms
Commands occasionally vanishCS or CKE dropped mid-assemblyerr_truncated, err_cke_low_mid
A command the controller never issued appearsMalformed assembly published — must not happenP4; if it fires, the abandon path is broken
Command bus saturated, banks idleA stream of four-cycle commands — §2cnt_cmds_4cy against cy_bus_occupied
Half the expected bandwidthOnly one channel in usecy_only_a and cy_only_b against cy_both_busy
Both channels work, performance is single-channelSerialised scheduler — §17cy_both_busy is zero
Single-stream bandwidth good, multi-requester latency poorAddress mapping spreads every access across both channels§9's callout — the mapping gave up independence
Valid data returned, wrong contentsChannel selection bug — §6Not visible at the interface. Check err_wrong_channel_*, then the mapping
Calibration behaves erraticallyTwo channels contending for ZQerr_zq_both — §7
Reinitialising one channel lost the other's dataRESET_n is shared — §7Expected; there is no per-channel reset
Bandwidth much higher than payload deliveredSub-granule requests — §8bytes_moved against bytes_requested; cnt_sub_granule

Row seven is the entry this chapter exists to add to a debugging table, and row nine is the one to fear. “Valid data, wrong contents” has no interface signature at all — every signal is legal, every transaction completes, and the only way in is the address mapping.

19. Misconceptions

“LPDDR4 reduced the command bus from 10 pins to 6.” §4. Per channel, yes. For a x32 interface the total went from 10 to 12 — the narrowing is what made two channels affordable, not a saving in itself.

“A narrower command bus is strictly worse.” §2. A long LPDDR4 command carries 24 bits against LPDDR3's 20. The bus carries more information per command and less per clock.

“Commands have a fixed length.” §2. One, two or four cycles, per command. A fixed encoding would have to be as long as the longest.

“Single-edge sampling is a regression.” §3. It trades edges for cycles, and on a device with no DLL the negative edge is the expensive one. The causal link is an inference and §3's callout labels it.

“The two channels are two halves of one interface.” §5. Separate command buses, clocks, clock enables and address spaces. 24.2's state machine is instantiated per channel.

“Independent means nothing is shared.” §7. RESET_n and ZQ are shared on the named part, and both have controller consequences.

“You can reset one channel.” §7. There is no per-channel reset. Recovering Channel A costs Channel B's contents.

“If both channels work, the architecture is being used.” §17. A serialised scheduler passes every assertion with cy_both_busy at zero and delivers single-channel performance.

“Spreading accesses across both channels is the optimisation.” §9's callout. It maximises single-stream bandwidth and destroys the independence the architecture exists to provide.

“16n prefetch means bursts are usually 32 bytes.” §8. It means 32 bytes is the smallest thing the channel can move. A one-byte read moves 32.

“Over-asking by a byte costs a byte.” §16's row fourteen. A 33-byte request moves 64 bytes. Waste is quantised, not proportional.

“A wrong-channel access will be caught.” §6's callout. It returns valid data from a valid address and is indistinguishable from a correct access at the interface.

20. Interview Reasoning

How wide is the LPDDR4 command bus, and how long is a command? Six bits per channel, and a command takes one, two or four clock cycles, sampled on the positive edge only. LPDDR3 used ten bits, both edges, one cycle.

Does LPDDR4 use fewer command pins than LPDDR3? Per channel yes, ten to six. For a x32 interface, no — twelve against ten, because LPDDR4 is two channels. The extra two pins buy a second independent command stream.

Why would a design give up double-edge command sampling? It trades edges for cycles. Four positive edges are four well-defined sampling instants; two edges of two clocks need the negative edge to be as trustworthy as the positive one, which is harder at 4266 Mbps on a device with no DLL.

What is the cost of the multi-cycle encoding? A command-rate ceiling. A stream of four-cycle commands issues at one quarter of the clock rate, because the bus is occupied for four cycles and cannot be shared.

What does channel independence require of a controller? No state crosses, no timing crosses, and the address spaces do not overlap — which in practice means two schedulers, two state machines, and an address decode that is a function of the assigned bits alone.

What is shared between the channels? RESET_n and ZQ on the named part. So there is no per-channel reset, and two channels can contend for one calibration reference with no arbitration in the interface.

How would you tell whether a controller is actually using both channels? Count cycles where both command buses are busy. Every independence assertion passes on a serialised controller; only that count distinguishes two channels from one channel with two data buses.

What is the minimum transfer on a x16 LPDDR4 channel, and why does it matter? Thirty-two bytes, from the 16n prefetch. A four-byte request moves 32 bytes and wastes 87.5% of the energy it spends, which is why small accesses are the mobile energy-per-bit problem.

Why are two x16 channels better than one x32 channel for mobile? They can serve two requesters at a 32-byte granule each, or behave like one wide channel at 64 bytes. One wide channel has only the second option, and a phone's traffic is many small unrelated requests.

What failure mode does channel selection have, and can you detect it? A legal address decoded to the wrong channel returns valid data from a valid location. It is undetectable at the interface, so the only defence is checking that selection depends on the assigned address bits and nothing else.

21. Exercises

  1. §3 infers that single-edge sampling and the absent DLL are the same decision. Construct the strongest argument against that inference using only facts in this chapter, and say what evidence would settle it.

  2. Compute the total CA pin count for a x64 interface built from LPDDR4 channels and from hypothetical x32 LPDDR3 devices. At what interface width, if any, does LPDDR4's per-channel narrowing produce a total pin saving?

  3. A workload issues four-cycle commands 30% of the time and one-cycle commands otherwise. Derive the maximum command rate in commands per clock, then find the four-cycle fraction at which the command bus rather than the array becomes the limit, stating the array assumption you must add.

  4. Remove the combinational fold of §12 and replace it with out_bits <= acc. Which of P1 through P7 fire, and which do not? Explain why P3 does not.

  5. §13's checker cannot detect a wrong-channel access. Design the smallest addition to a system — not to this block — that would detect it, and state what it costs.

  6. Using §14's accountant, compute total bytes moved for a stream of 1,000 requests uniformly distributed over 1–64 bytes. Compare against the payload requested and express the result as a pair rather than a ratio, naming your denominator.

  7. §9's callout says an interleaving address map destroys independence. Construct a hybrid map that preserves independence for accesses below some size and interleaves above it, then identify the property of the requester mix that decides whether it helps.

  8. A controller reports cy_both_busy at 12% of cy_only_a. Without seeing the design, list the three most likely causes in order and the measurement that distinguishes them.

22. Where This Goes

The LPDDR4 interface is now concrete rather than asserted. Six command pins per channel, commands over one, two or four positive clock edges, two channels whose independence is an invariant with exactly two named exceptions, and a 32-byte floor on anything the channel can move.

Three results carry forward. The narrow bus is an enabler rather than a saving — a x32 interface uses two more CA pins than the previous generation and buys a second command stream with them. The command bus is a schedulable resource, with a 0.25 commands-per-clock ceiling on the longest commands that a controller can genuinely saturate. And independence is a property a correct controller can waste entirely, with no assertion firing and nothing but a zero in an occupancy counter to show it.

One thing this chapter established and could not finish. Chapter 24.1 §11 excluded “wasted payload on small accesses” from its energy accountant because nothing could compute it; §8 and §14 now compute the bytes. Converting those bytes into energy still needs a per-bit figure no datasheet in this module publishes, so the term remains excluded from the energy total while being fully measured in payload. That is progress and not closure, and 24.5 does not close it either.

What every section here assumed is that the data bus and the command bus share a clock. On the next generation they do not. Chapter 4.7 §3 noted that LPDDR5 introduces a separate write clock and stated that Module 24 owns that machinery — this is where it is owed.

Chapter 24.4 takes it up: a WCK running at two or four times the command clock, a ratio that is itself configurable, one x16 channel per die where LPDDR4 had two, DDR command entry where LPDDR4 was single-edge, a burst length of 16 or 32, and a data-bus inversion encoding whose benefit depends on the data. Two clock domains with a configurable integer ratio is a different design problem from anything in this module so far, and the block that manages the crossing is where it becomes visible.

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.