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:
| LPDDR3 | LPDDR4 | |
|---|---|---|
| Command/address bus | CA0–CA9 — 10 bits | CA[5:0] — 6 bits |
| Clock cycles per command | 1 | 1, 2 or 4 |
| Sampled on | both clock edges | positive 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:
| Interface | Pins | Edges | Cycles | Bits per command |
|---|---|---|---|---|
| LPDDR3 | 10 | 2 | 1 | 20 |
| LPDDR4, short | 6 | 1 | 1 | 6 |
| LPDDR4, medium | 6 | 1 | 2 | 12 |
| LPDDR4, long | 6 | 1 | 4 | 24 |
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 CA0–CA9.
DERIVED, recomputed:
| Interface at x32 | CA pins | Command streams |
|---|---|---|
| LPDDR3, one x32 channel | 10 | 1 |
| LPDDR4, two x16 channels | 2 × 6 = 12 | 2 |
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 busCK_t/CK_cclock pairCKEclock enableCSchip selectDQ[15:0]data bus with its strobesDMI[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.
| Signal | Scope | Consequence |
|---|---|---|
CA[5:0], CK, CKE, CS, DQ, DMI | per channel | genuine independence |
RESET_n | shared | resets both channels together |
ZQ | shared | the 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:
16n prefetch on a x16 channel
= 16 transfers x 16 bits
= 256 bits
= 32 bytes minimum per channel burstThirty-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:
| Request | Bursts | Bytes moved | Wasted | Useful |
|---|---|---|---|---|
| 4 B | 1 | 32 | 28 | 12.5% |
| 8 B | 1 | 32 | 24 | 25.0% |
| 16 B | 1 | 32 | 16 | 50.0% |
| 32 B | 1 | 32 | 0 | 100% |
| 64 B | 2 | 64 | 0 | 100% |
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:
| Organisation | Minimum granule | Requesters served at once |
|---|---|---|
| One x32 channel | 64 B | 1 |
| Two x16 channels, one access spanning both | 64 B | 1 |
| Two x16 channels, serving separately | 32 B each | 2 |
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
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
// ---------------------------------------------------------------------
// 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;
endmodule12. 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:
out_bits <= acc | (CMD_W'(ca) << (got*CA_W));rather than
out_bits <= acc; // WRONG -- one sample shortThe 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
// ---------------------------------------------------------------------
// 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;
endmodulereset_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
// ---------------------------------------------------------------------
// 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;
endmodule15. What the Assertions Prove
// 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
| Case | Behaviour | Why |
|---|---|---|
| One-cycle command | Completes on its start edge; bus never busy | P5 — it cannot be interleaved with anything |
CS deasserted mid-command | err_truncated; nothing published | P4 — a fabricated command is worse than a lost one |
CKE low mid-command | err_cke_low_mid; assembly abandoned | The remaining samples will not arrive |
| New command asserted mid-command | err_new_cmd_mid; in-flight command continues | The bus carries one; dropping the in-flight one loses a real command |
| Unknown length field | err_unknown_length; nothing assembled | Guessing a length would mis-frame every following command |
Final sample folded from acc | Every multi-cycle command short six bits | §12's hazard; P7 catches it |
| Back-to-back four-cycle commands | 0.25 commands per clock | §2's ceiling — a real scheduling limit |
| Both channels busy simultaneously | Legal and counted | P8 — the architecture's purpose |
Both channels request ZQ | err_zq_both; one grant only | §7 — the shared resource needs serialising |
| Reset requested | reset_both — both channels | §7 — no per-channel reset exists |
| Legal address on the wrong channel | Undetectable here; valid data returned | §6's callout — the silent failure |
| Request of 1 byte | One 32 B burst; 31 B wasted | §8 — 32 B is the minimum, not the minimum efficient |
| Request of exactly 32 B | One burst, zero waste | The granule boundary |
| Request of 33 B | Two bursts, 31 B wasted | Crossing a granule costs a whole granule |
| Zero-length request | err_zero_length_req; nothing moved | P14 — not silently rounded to a granule |
GRAN_B not a power of two | $fatal at elaboration | Rounding 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
// 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| Check | What it establishes |
|---|---|
| Every command the DUT publishes matches the reference's assembly | Two representations agree |
| Publish counts match over a long random stream | No command silently lost or duplicated |
| Assemble all three lengths; compare bit-for-bit | The three length covers, and P7 on the four-cycle case |
| Truncate at each cycle position of a four-cycle command | err_truncated at every position, nothing published |
Drop CKE at each cycle position | err_cke_low_mid at every position |
| Assert a new command at each mid-command position | err_new_cmd_mid; the in-flight command still completes |
| Drive the unknown length encoding | err_unknown_length; the following command frames correctly |
| Back-to-back four-cycle stream; measure commands per clock | The 0.25 ceiling, measured rather than asserted |
| Drive both channels simultaneously; confirm no violation | P8 and the independence cover |
Serialise both channels through one scheduler; observe cy_both_busy | The failure a passing assertion suite hides |
Both channels request ZQ on the same cycle, repeatedly | P10, 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 B | The quantisation of §16's row fourteen |
The tenth check produces the report that matters most, because it is a failure no assertion reports:
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
| Symptom | Likely cause | How to confirm |
|---|---|---|
| Short commands decode, long ones do not | §12's stale-register fold | cmd_bits top field zero on four-cycle commands; P7 |
| Every command after a certain point is wrong | A length mis-framed, shifting all subsequent framing | err_unknown_length; check the first bad command, not the symptoms |
| Commands occasionally vanish | CS or CKE dropped mid-assembly | err_truncated, err_cke_low_mid |
| A command the controller never issued appears | Malformed assembly published — must not happen | P4; if it fires, the abandon path is broken |
| Command bus saturated, banks idle | A stream of four-cycle commands — §2 | cnt_cmds_4cy against cy_bus_occupied |
| Half the expected bandwidth | Only one channel in use | cy_only_a and cy_only_b against cy_both_busy |
| Both channels work, performance is single-channel | Serialised scheduler — §17 | cy_both_busy is zero |
| Single-stream bandwidth good, multi-requester latency poor | Address mapping spreads every access across both channels | §9's callout — the mapping gave up independence |
| Valid data returned, wrong contents | Channel selection bug — §6 | Not visible at the interface. Check err_wrong_channel_*, then the mapping |
| Calibration behaves erratically | Two channels contending for ZQ | err_zq_both — §7 |
| Reinitialising one channel lost the other's data | RESET_n is shared — §7 | Expected; there is no per-channel reset |
| Bandwidth much higher than payload delivered | Sub-granule requests — §8 | bytes_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
-
§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.
-
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?
-
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.
-
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. -
§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.
-
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.
-
§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.
-
A controller reports
cy_both_busyat 12% ofcy_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
Related tutorials
- Related topic
DDR (DDR1)
Double data rate doubles transfer opportunities per clock cycle, not the clock. Two mechanisms make that survivable: a 2n prefetch so a slow array can feed a fast interface, and a source-synchronous DQS strobe so data carries its own timing.
- Related topic
DDR2
A clock cycle has only two edges, so DDR2 raised bandwidth by deepening prefetch to 4n — making prefetch depth the frequency ratio between interface and core. Raising the rate on a multi-load bus then forced termination onto the die.
- Related topic
Burst Reads
One read command returns several transfers because the array moves more data per access than the interface is wide. That makes beat counting a correctness obligation, not bookkeeping.
- Related topic
Burst Length
Burst length counts transfer positions. It is not a byte count, not a bus width, not a cache line, and not a number of clock cycles — and it is not a free menu you pick from.
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.
