DDR · Module 14
tRP — Row Precharge Time
A controller can mark a bank closed the instant it issues PRECHARGE and be right about the state, while being wrong if it reads CLOSED as ACTIVATE-legal-now. tRP is where state and timing diverge most visibly.
Chapter 14.1 populated the edge that opens a row's useful life. This chapter populates the one that ends it, and it carries a lesson the curriculum has been building toward since Module 9:
A controller may mark a bank CLOSED the instant it issues the PRECHARGE, and be entirely correct about the state — while being entirely wrong if it reads CLOSED as “ACTIVATE legal now.”
Those two sentences are both true at once, and holding them together is the whole of this chapter. tRP is where Chapter 13.1's separation of state legality from timing legality stops being a framework and becomes a bug you can point at.
1. The Obligation, in Five Parts
The grammar from Chapter 14.1 §1, applied to tRP:
PARAMETER tRP
1 triggering event PRECHARGE accepted on bank B
2 constrained event ACTIVATE addressed to bank B
3 resource bank B BANK-LOCAL
4 magnitude published in NANOSECONDS, resolved to cycles
5 sense MINIMUM SEPARATION
─────────────────────────────────────────────────────────────────
consequence ACTIVATE to bank B not before
pre_cycle + ceil(tRP / tCK)Structurally this is the same shape as tRCD — same scope, same units, same sense, same conversion. That similarity is worth noticing rather than skipping, because it is the first evidence that the module is not ten unrelated facts: two parameters, two different edges, one identical grammar.
And the four follow-up questions:
What re-arms it? Another PRECHARGE to the same bank. Legal but pointless — precharging an already-precharged bank is permitted and simply re-arms the obligation, which is a real way to make a controller slower than it needs to be.
What can dominate it? Chapter 14.4's tRC, whose ACTIVATE-to-ACTIVATE deadline can be later than tRP's, and Chapter 14.7's activate-spacing obligations, which are not bank-local at all. tRP is frequently not the controlling constraint on the next ACTIVATE, which is Chapter 14.4's entire subject.
Cycle or absolute-time component? Absolute time, published in nanoseconds — verified in §4.
Does generation matter? Not for the structure. The value differs by speed grade, as all of these do.
2. Two Questions That Look Like One
Here is the situation that makes this chapter necessary.
A controller decides to close bank 5 — a row miss, say, needing a different row. It issues PRECHARGE to bank 5 at cycle 200. The device accepts it.
Now: what is bank 5's state?
The honest answer is that this depends on what you mean, and the two meanings come apart:
Architecturally, bank 5 is no longer open. There is no row usable in its sensing circuitry. A column command to bank 5 is now meaningless — there is nothing to select from. Every reasonable state model marks the bank CLOSED here, and a controller that kept it OPEN would issue column commands against nothing.
Temporally, bank 5 is not yet reusable. The ACTIVATE that would open the next row is illegal until 200 + ceil(tRP / tCK).
This is worth contrasting with Chapter 14.1, where the same structure appears with the polarity reversed. There, the bank is OPEN and a column command is still illegal — state permits, timing forbids. Here, the bank is CLOSED and an ACTIVATE is still illegal — again state permits (a closed bank is exactly what ACTIVATE wants) and timing forbids.
Both directions of the row lifecycle have a state-permits-timing-forbids window. That symmetry is not a coincidence; it is what it means for a command to start an operation rather than perform one.
3. Why the Obligation Exists
One paragraph, architectural.
PRECHARGE does not instantaneously return a bank to a reusable condition. It starts the process of returning the bitlines to the prepared state that the next sensing operation requires. Chapter 3.3 covered the differential pair and what it must look like before a row can be sensed against it; Chapter 2.6 covered why the row's data must be intact in the cells before the sensing circuitry lets go of it. A following ACTIVATE asks the device to begin sensing. Begin too early and it senses against a bitline that is not prepared.
The controller models none of this. It holds a nanosecond figure from a speed-bin table and counts, exactly as Chapter 13.2 §1 established.
4. Units — Verified
So the conversion is Chapter 13.2 §3's ceiling again:
tRP_cycles = ceil( tRP_ns / tCK )At DDR4-3200, tCK is 0.625 ns and 13.75 ns resolves to exactly 22 cycles. At DDR4-2400, tCK is 0.8333 ns and it resolves to 17 — 16.5 rounded up. Same physical requirement, different cycle count, and the faster part needs more cycles.
The fact that tRP and tRCD happen to share a value at one speed grade is a trap worth naming. A controller that stores one register for “the row timing value” because the two matched in the part it was brought up on will be wrong on the next part. They are independent parameters that happen to coincide.
5. The Exact Legal Boundary
PRECHARGE accepted (sampled) on cycle N
resolved requirement D cycles
ACTIVATE sampled at M is LEGAL ⟺ M - N >= D
earliest legal M = N + D
forbidden cycles = N … N+D-1 exactly D of themThe same contract as Chapter 14.1 §5, because it is the same convention — and keeping it the same across all ten parameters is a deliberate choice. A module in which different parameters used different boundary conventions would be worse than useless.
Notice the two refusals in that diagram carry different reasons, and Chapter 13.1 §6 showed they demand opposite responses. The column command's refusal is permanent until somebody does work — an ACTIVATE. The ACTIVATE's refusal clears on its own with nothing changed. A controller that reported both as “bank busy” could not tell which.
6. RTL — Keeping State and Timing Separate by Construction
Collision check. Chapter 14.1's act_to_column_guard is a per-bank countdown from an ACTIVATE. Chapter 9.1's bank_row_fsm and Chapter 5.2's ddr_bank_state_table own bank state. Chapter 13.3's deadline_scoreboard holds abstract obligations.
A plain per-bank tRP countdown would be act_to_column_guard with two signal names changed, and building that would be exactly the “ten disconnected countdown timers” architecture this module is supposed to teach engineers to avoid.
So this block does the thing that is genuinely new here: it publishes state and timing permission as two separate outputs and makes it structurally impossible to read one as the other.
The engineering problem. A consumer needs to know three different things about a bank — is a row usable, may a column command issue, may an ACTIVATE issue — and a single state enum answers only the first. Designs that expose one bank_busy bit force every consumer to guess.
Classification: controller-side sequential bookkeeping over state and time. No physical modelling.
// ─────────────────────────────────────────────────────────────────────
// bank_close_state_vs_timing
//
// CLASSIFICATION
// Controller-side sequential bookkeeping. Tracks an architectural
// state bit and a timing countdown, per bank, and publishes them
// SEPARATELY.
//
// WHAT IT MODELS
// §2's divergence, made structural:
// trigger = PRECHARGE accepted on bank b
// constrained = ACTIVATE addressed to bank b
// scope = BANK-LOCAL
// sense = MINIMUM SEPARATION
// plus the architectural fact that the bank is CLOSED from the
// moment of acceptance, which is a DIFFERENT fact with a DIFFERENT
// lifetime.
//
// WHAT IT DOES NOT MODEL
// Any physical process. No bitline, no charge, no sensing. There is
// no "precharge complete" input because the device provides no such
// signal -- the contract is a separation, not a completion, and a
// block with a completion port would be teaching a fiction.
//
// THE DESIGN DECISION THIS BLOCK EXISTS TO MAKE
// row_usable and activate_timing_ok are SEPARATE OUTPUTS. There is
// deliberately NO combined "bank_ready" output, because every
// consumer that wanted one would be conflating two questions whose
// refusals demand opposite responses (Chapter 13.1 §6). Omitting
// the convenient output is the point.
//
// CONVENTION (Chapter 13.2 §6)
// PRECHARGE accepted on cycle N with resolved magnitude D forbids
// an ACTIVATE on cycles N .. N+D-1 and permits it from N+D. The
// command bus carries one command per cycle, so cycle N cannot host
// a competing ACTIVATE; the countdown loads D-1.
//
// SIMULTANEITY
// precharge and expiry same cycle -> PRECHARGE WINS (re-arms).
// precharge to an already-closed bank -> legal, re-arms. This is a
// real and wasteful controller behaviour, so it is REPORTED on
// redundant_precharge rather than silently absorbed.
// activate and precharge same cycle -> impossible on one command
// bus; the input encoding cannot express it.
// reset -> all banks closed, all obligations abandoned. Closed is
// the safe reset state: it forbids column commands, which is
// the conservative direction.
// ─────────────────────────────────────────────────────────────────────
module bank_close_state_vs_timing #(
parameter int BANKS = 16,
parameter int MAX_CYCLES = 32,
parameter int BK_W = (BANKS <= 1) ? 1 : $clog2(BANKS),
parameter int CNT_W = (MAX_CYCLES <= 1) ? 1 : $clog2(MAX_CYCLES + 1)
) (
input logic clk,
input logic rst_n,
// ── Resolved tRP, in cycles. Chapter 13.2's resolver produced this
// from a value published in nanoseconds.
input logic [CNT_W-1:0] trp_cycles,
// ── Accepted commands. Acceptance, not request: an ACTIVATE the
// state layer refused must change nothing here.
input logic pre_accepted,
input logic [BK_W-1:0] pre_bank,
input logic act_accepted,
input logic [BK_W-1:0] act_bank,
// ── The candidate being evaluated.
input logic [BK_W-1:0] cand_bank,
// ── ARCHITECTURAL: is a row usable in this bank's sensing
// circuitry? Governs whether a COLUMN command is state-legal.
output logic row_usable,
// ── TEMPORAL: has tRP elapsed for this bank? Governs whether an
// ACTIVATE is timing-legal. Deliberately a separate output from
// row_usable -- see the header.
output logic activate_timing_ok,
output logic [CNT_W-1:0] activate_wait_remaining,
// ── Observability.
output logic [BANKS-1:0] bank_closed,
output logic [BANKS-1:0] bank_in_trp,
// A PRECHARGE to a bank that was already closed. Legal, and a
// wasted command that also RE-ARMS the obligation -- so it delays
// the next ACTIVATE for no benefit. Worth counting.
output logic redundant_precharge
);
if (BANKS < 1) begin : g_banks
initial $fatal(1, "bank_close_state_vs_timing: BANKS must be >= 1");
end
if (MAX_CYCLES < 0) begin : g_max
initial $fatal(1, "bank_close_state_vs_timing: MAX_CYCLES must be >= 0");
end
// ── Two pieces of per-bank state, tracking two different facts with
// two different lifetimes. The separation in the DECLARATION is
// what the whole chapter is about.
logic is_open [BANKS]; // architectural
logic [CNT_W-1:0] trp_cnt [BANKS]; // temporal
always_comb begin
for (int unsigned b = 0; b < BANKS; b++) begin
bank_closed[b] = !is_open[b];
bank_in_trp[b] = (trp_cnt[b] != '0);
end
// Applicability by resource: only the candidate's own bank.
row_usable = is_open[cand_bank];
activate_wait_remaining = trp_cnt[cand_bank];
activate_timing_ok = (trp_cnt[cand_bank] == '0);
redundant_precharge = pre_accepted && !is_open[pre_bank];
end
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
for (int unsigned b = 0; b < BANKS; b++) begin
is_open[b] <= 1'b0;
trp_cnt[b] <= '0;
end
end else begin
// Age first, then arm -- so a fresh PRECHARGE is not decremented
// on its own arming cycle. Reversing these under-blocks by one.
for (int unsigned b = 0; b < BANKS; b++) begin
if (trp_cnt[b] != '0) begin
trp_cnt[b] <= trp_cnt[b] - 1'b1;
end
end
// ARCHITECTURAL transition and TEMPORAL arming happen on the
// same cycle and are still two distinct updates. Writing them
// as two statements rather than one is deliberate.
if (pre_accepted) begin
is_open[pre_bank] <= 1'b0; // immediate
trp_cnt[pre_bank] <= (trp_cycles >= CNT_W'(1)) // and a wait
? (trp_cycles - CNT_W'(1))
: '0;
end
// An accepted ACTIVATE opens the bank. Note it does NOT clear
// trp_cnt: by the convention, an ACTIVATE cannot have been
// accepted while trp_cnt was nonzero, so there is nothing to
// clear. §7's third property is what makes that safe to assume
// rather than merely hope.
if (act_accepted) begin
is_open[act_bank] <= 1'b1;
end
end
end
endmoduleInterface contract. row_usable and activate_timing_ok are both indexed by cand_bank and answer different questions about it. There is no combined output, and a consumer that wants one must write the conjunction itself — at which point it has to think about which layers it is conjoining, which is the intended friction.
Parameter contract. CNT_W sized as a count, $clog2(MAX_CYCLES + 1), for the reason Chapter 14.1 §7 gave: a magnitude equal to a power-of-two MAX_CYCLES would otherwise truncate to zero and forbid nothing.
Internal state. Two arrays, and the fact that they are two is the design. is_open is a single bit with an event-driven lifetime — set by ACTIVATE, cleared by PRECHARGE. trp_cnt is a counter with a time-driven lifetime. One is a fact about what is there; the other is a fact about how long ago something happened. A design with one array cannot express both.
Reset behaviour. All banks closed, all counters zero. Closed is the right reset state because it forbids column commands — the conservative direction. Note that reset does not arm tRP: a bank that was never precharged has no obligation, and arming one would insert a phantom delay at every reset.
Corner cases. PRECHARGE to an already-closed bank: legal, sets is_open to a value it already had, and re-arms the countdown — so it genuinely delays the next ACTIVATE. redundant_precharge reports it because this is a real controller inefficiency, not a theoretical one: a design that precharges defensively without checking state pays tRP repeatedly. trp_cycles == 0 or 1: forbids nothing observable, as in Chapter 14.1. BANKS == 1: degenerates correctly.
Failure modes. The headline one: deriving activate_timing_ok from !is_open. That is the conflation this block exists to prevent, and it produces an ACTIVATE issued on the cycle after the PRECHARGE — a violation by nearly the whole of tRP, on every row miss. It is also very easy to write by accident, because !is_open is almost the right condition and is true in a superset of the correct cycles. Second: clearing trp_cnt on act_accepted “for tidiness”, which does nothing when the design is correct and masks the violation when it is not.
7. Four Assertions Worth Writing
// ── P1. THE property of this chapter: closed does not imply
// ACTIVATE-legal. Asserted as a reachability claim rather than an
// implication, because the whole point is that the two conditions
// COME APART -- and a design that conflated them would make this
// cover unreachable.
// Written as a cover, not an assert: it must be OBSERVED, and an
// implication cannot express "these must sometimes differ".
generate
for (genvar gb = 0; gb < BANKS; gb++) begin : g_divergence
// A bank that is closed while its tRP obligation is outstanding.
// If this never covers, the testbench never exercised the window
// that this chapter is about, and P2 below proves nothing.
c_closed_but_not_activatable:
cover property (@(posedge clk) disable iff (!rst_n)
(!is_open[gb] && (trp_cnt[gb] != '0)) );
end
endgenerate
// ── P2. No ACTIVATE is accepted while its bank's tRP is outstanding.
// The safety property. Catches the conflation failure directly: a
// design deriving permission from !is_open fires this on the first
// row miss.
property p_no_activate_during_trp;
@(posedge clk) disable iff (!rst_n)
act_accepted |-> (trp_cnt[act_bank] == '0);
endproperty
a_no_activate_during_trp: assert property (p_no_activate_during_trp);
// ── P3. The obligation is exactly §5's length, checked from the
// arming event in both directions. Catches both off-by-one signs.
generate
for (genvar gc = 0; gc < BANKS; gc++) begin : g_boundary
property p_trp_length_exact;
@(posedge clk) disable iff (!rst_n)
( $past(pre_accepted, 1) && ($past(pre_bank, 1) == BK_W'(gc))
&& ($past(trp_cycles, 1) >= CNT_W'(2)) )
|-> (trp_cnt[gc] == ($past(trp_cycles, 1) - CNT_W'(1)));
endproperty
a_trp_length_exact: assert property (p_trp_length_exact);
end
endgenerate
// ── P4. SCOPE: a PRECHARGE to one bank never arms another's
// obligation, and never changes another's open state. Chapter 13.3
// §4's Bug 1 for this parameter -- the safe-and-slow failure.
generate
for (genvar gd = 0; gd < BANKS; gd++) begin : g_scope
property p_precharge_is_bank_local;
@(posedge clk) disable iff (!rst_n)
( $past(pre_accepted, 1) && ($past(pre_bank, 1) != BK_W'(gd))
&& ($past(trp_cnt[gd], 1) == '0) )
|-> ( (trp_cnt[gd] == '0)
&& (is_open[gd] == $past(is_open[gd], 1)) );
endproperty
a_precharge_is_bank_local: assert property (p_precharge_is_bank_local);
end
endgenerateWhat these prove. That the divergence window is actually reached by the stimulus (P1, a cover rather than an assert — the distinction matters and is explained below); that no ACTIVATE slips into it; that the window is exactly the stated length; and that the obligation and the state change are both bank-local.
Why P1 is a cover and not an assertion. The claim “closed does not imply activate-legal” is not a safety property — it is a statement that two conditions differ somewhere. An assertion cannot express that: !is_open |-> trp_cnt != 0 is false whenever a bank has been closed for a long time, and !is_open |-> trp_cnt == 0 is the conflation bug itself. What you actually need is evidence that the divergent state was visited, which is a cover. A testbench in which this cover never hits has not tested this chapter, and P2 would then be passing vacuously.
What these do not prove. That trp_cycles is right — it is an input, and Chapter 14.1 §9 covered why no block-level property can see that. That pre_accepted reflects genuine acceptance. And nothing about tRC or the activate-spacing obligations, which can push the next ACTIVATE later than tRP alone would (Chapter 14.4).
8. The Divergence, in Cycles
bank_close_state_vs_timing — closed is not activatable
10 cyclesCycles 3 through 5 are the whole chapter. is_open is already low — the state model has correctly recorded that no row is usable. act_timing_ok is also low. A design that computed activate permission as !is_open would have permitted an ACTIVATE at cycle 3, three cycles early, on every row miss the controller ever performs.
Cycles 6 and 7 are the other half. The bank is still closed and now activatable. Here !is_open and act_timing_ok agree — which is exactly why the conflation bug is hard to see. The two signals agree most of the time, and differ only inside the window that matters.
9. DV — Independently Reconstructing tRP
The Chapter 13.4 §8 discipline, specialised.
Invert the representation. The design counts down; the checker records pre_cycle[bank] and subtracts.
Resolve the magnitude independently from nanoseconds, as Chapter 14.1 §10 argued. This matters more for tRP than for most parameters because of §4's coincidence: tRP and tRCD share a value at some speed grades, so a design that mistakenly uses one register for both passes every tRP check and every tRCD check at that grade and fails at the next. A checker that resolves each parameter from its own table entry catches the shared-register bug; a checker that reads the design's register cannot.
Track state and timing separately in the checker too. If the checker keeps one bank_busy notion, it cannot distinguish a state violation from a timing violation, and its reports will misdirect. Two pieces of model state, mirroring §6's two arrays — not because the checker copies the design, but because the problem has two facts in it.
A report that earns its keep:
TIMING VIOLATION
command : ACTIVATE, bank 4
issued at : cycle 203
violated : tRP
resource : bank 4 (bank-local)
source event : PRECHARGE bank 4, cycle 200
requirement : 13.75 ns / 0.625 ns = 22 cycles (ceil)
legal from : cycle 222
short by : 19 cycles
note : bank state was CLOSED and correct at issueThat last line is the one to steal. A shortfall of nearly the full requirement, with the state correct, is the signature of the conflation bug — permission derived from state rather than from elapsed time. A checker that says so converts a puzzling violation into a one-line diagnosis.
10. Debugging
Symptom. ACTIVATE commands are violating tRP, and the bank-state view shows the bank correctly closed at the moment of issue.
| Candidate mechanism | Evidence | Discriminator |
|---|---|---|
| Permission derived from state — §6's headline failure | Shortfall is close to the whole requirement, every time | The most decisive check in this chapter: is the shortfall roughly D, or roughly 1? Near-D means the obligation was never consulted. |
| Off-by-one in the convention | Shortfall is exactly 1, consistently | Count forbidden cycles. §5 fixes it at exactly D. |
| tRP and tRCD sharing one register | Correct at a grade where they coincide; wrong elsewhere | Compare the two table entries at the failing grade. §4's coincidence. |
| Armed on request, not acceptance | Violations only where a PRECHARGE was refused | What drives pre_accepted. |
| Scope error — shared countdown | Several banks blocked by one PRECHARGE | Throughput against bank count. §7's P4. |
| Redundant precharges extending the wait | No violation; just unexplained slowness on row misses | redundant_precharge count. Each one re-arms tRP for no benefit. |
| Stale value after frequency change | Violations begin at a frequency transition | Whether the resolver re-ran. |
The discriminator that resolves this fastest is the size of the shortfall. Near-D means the timing obligation played no part in the decision — look at what produces the permission signal. Exactly 1 means the obligation was consulted and its boundary is off by one — look at the counting convention. Those two lead to completely different files, and the distinction costs one glance at a violation report.
Responsible layer. If redundant_precharge is nonzero and there are no violations, nothing is broken — the controller is issuing precharges it did not need, which is a policy question for Module 17, not a timing bug.
11. Common Misconceptions
“If PRECHARGE was issued, ACTIVATE is immediately legal.”
Tempting because the bank is closed, and closed is the precondition ACTIVATE wants. Why it is wrong: closed answers “is a row usable”; tRP answers “may I start opening one”. Different questions, §2. Consequence: a violation of nearly the full tRP on every row miss — the most common timing bug in a first controller. Replacement model: state and timing are separate outputs. Debugging clue: the shortfall is close to the whole requirement rather than one cycle.
“tRP is how long a precharge takes.”
Tempting because the name suggests a duration and there is a real process underway. Why it is wrong: it is a minimum separation before the next ACTIVATE. There is no completion signal and no way to observe whether the device finished sooner. Consequence: engineers look for a completion indication, or assume a faster device will report readiness. Replacement model: a separation the controller counts. Debugging clue: any proposed design that waits for a “precharge done” input is modelling something that does not exist.
“tRP and tRCD are the same value, so one register is enough.” Tempting because §4 shows them coinciding at a real DDR4-3200 speed grade. Why it is wrong: coincidence at one grade, not identity. They constrain different edges and are published independently. Consequence: correct on the bring-up part and wrong on the next one, with violations on whichever parameter is larger. Replacement model: two parameters, two registers, resolved separately. Debugging clue: a design that worked on one part and fails on another with no code change.
“PRECHARGE-ing a closed bank is harmless.”
Tempting because it is legal and the state is already correct, so it looks like a no-op. Why it is wrong: it re-arms tRP. The bank was activatable and is now not, for the full requirement. Consequence: a defensive controller that precharges without checking state delays its own next ACTIVATE, repeatedly, invisibly. Replacement model: a redundant precharge costs a full tRP. Debugging clue: row-miss latency higher than the model, with no violations anywhere.
“If a READ is legal, a PRECHARGE must be too.”
Tempting because both concern the same open bank and reading is the “bigger” operation. Why it is wrong: they are constrained by different obligations — the column command by tRCD, the precharge by tRAS and, after a write, by tWR. These expire at different times. Consequence: a precharge issued as soon as the row has been read, violating the minimum active interval. Replacement model: every command class has its own applicable set. Debugging clue: violations on precharge immediately following a successful read — which is Chapter 14.3's opening example.
12. Interview Reasoning
“What does tRP constrain?”
The five parts: an accepted PRECHARGE on a bank is the trigger; an ACTIVATE to that bank is the constrained event; the resource is the bank, so it is bank-local; the magnitude is published in nanoseconds and resolved by a ceiling; and it is a minimum separation. Earliest legal ACTIVATE is the precharge cycle plus the resolved count.
“Why can a bank be logically closed while an ACTIVATE is still too early?”
Because the two statements answer different questions over different information. CLOSED is a claim about what is present in the bank right now — no usable row — and it is correct from the moment the command is accepted. tRP is a claim about elapsed time since that acceptance, and no state enum records when a state was entered. The design consequence is that activate permission must come from a counter, never from the state bit, and the failure mode when it does not is a violation of nearly the whole of tRP on every row miss.
“A colleague computes activate_ok = !bank_open. What breaks, and how big is the error?”
Every ACTIVATE after a PRECHARGE becomes legal one cycle after the precharge instead of tRP cycles after it — so the error is roughly the entire requirement, twenty-something cycles at a typical DDR4-3200 grade. It is a particularly nasty bug because the expression is almost right: the two conditions agree everywhere except inside the tRP window, so most traffic looks fine and functional simulation against an untimed model shows nothing.
“Why is PRECHARGE-ing an already-closed bank not free?”
Because acceptance re-arms the obligation. The bank was activatable; now it must wait the full tRP again. Nothing is incorrect and the controller has simply delayed itself. This is worth raising unprompted in any conversation about defensive precharging, because it is the cost that makes “just precharge to be safe” a bad default.
“How would you catch a design that used one register for both tRP and tRCD?” Not with a block-level assertion, because at a speed grade where the two values coincide the design is arithmetically correct and every property passes. You catch it by resolving each parameter independently from its own speed-bin entry in the checker, and by running a grade where the two differ. This is the concrete case for why a checker must not read the design's configured values.
13. Engineering Exercises
1. Resolve and bound. tRP is 13.75 ns on a DDR4-2933 part where tCK is approximately 0.682 ns. A PRECHARGE is accepted at cycle 500. Give the resolved cycles, the forbidden range, and the earliest legal ACTIVATE.
Worked: 13.75 / 0.682 = 20.16, ceiling 21 cycles. Forbidden: cycles 500 through 520 — twenty-one of them. Earliest legal ACTIVATE: cycle 521. Quantisation waste is 21 × 0.682 − 13.75 = 0.57 ns.
2. Size the conflation bug. Using exercise 1's figures, a design computes activate permission as !bank_open. By how many cycles does it violate, and what would the violation report's shortfall field read?
Worked: the bank is closed from cycle 501, so the design permits an ACTIVATE at 501 where 521 is the earliest legal — short by 20 cycles. The report's shortfall is 20, which is one less than the requirement. A shortfall of D − 1 is the fingerprint of this bug, and recognising that number on sight is worth more than the arithmetic.
3. Separate the questions. For a bank in each of these conditions, say whether a column command is legal and whether an ACTIVATE is legal: (a) open, tRCD elapsed; (b) open, tRCD outstanding; (c) closed, tRP outstanding; (d) closed, tRP elapsed.
Worked: (a) column yes, activate no — the bank is open, so activating it is a row conflict, refused by the state layer. (b) column no (timing), activate no (state). (c) column no (state), activate no (timing). (d) column no (state), activate yes. Four conditions, four different answer pairs, and no single “ready” bit can encode them — which is §6's argument for two outputs, arrived at from the outside.
4. Count the cost of defensiveness. A controller precharges a bank before every ACTIVATE, without checking whether it is already closed. On a stream of accesses of which 40 percent are to already-closed banks, what is the added delay per redundant precharge, and how would you detect this in the field?
5. Write the cover, not the assertion. Explain why the claim “closed does not imply activatable” cannot be written as an SVA assertion, and what the corresponding cover establishes. Then say what it means if that cover never hits in a long random run.
Worked: the claim is that two conditions differ somewhere, which is existential, and assertions are universal. !is_open |-> trp_cnt != 0 is false for any long-closed bank; !is_open |-> trp_cnt == 0 is the bug. The cover establishes that the divergent window was visited. If it never hits, the stimulus never precharged a bank — so P2 has been passing vacuously and the tRP logic is entirely untested.
6. Find the shared-register bug by construction. Choose two DDR4 speed grades from a real vendor table where tRP and tRCD differ, and one where they coincide. Explain which grade must be in the regression and why testing only the other is worse than not testing.
7. Argue against the design. §6 deliberately omits a combined bank_ready output, forcing consumers to write the conjunction. Make the strongest case that this is unhelpful API design, then rebut it.
14. Summary
tRP is the mirror of tRCD and has the same five-part shape: an accepted PRECHARGE on a bank constrains a following ACTIVATE to that bank, with a magnitude published in nanoseconds, as a minimum separation. Verified: Micron's DDR4 speed-bin tables publish tRP in nanoseconds alongside tRCD and tRC, and at one DDR4-3200 grade it appears as 13.75 ns.
What makes this chapter different from its neighbour is the divergence it exposes. A bank is CLOSED from the instant the PRECHARGE is accepted, and not activatable until tRP has elapsed. Both facts are true simultaneously; they answer different questions — is a row usable versus may I start opening one — and no single state enum can carry both.
The failure that follows is the most common timing bug in a first DDR controller: deriving activate permission from !bank_open. It is easy to write because the two conditions agree everywhere except inside the window that matters, and its fingerprint in a violation report is a shortfall of D − 1 — nearly the whole requirement, rather than the one cycle a convention error would produce.
bank_close_state_vs_timing publishes the two facts as two outputs, from two arrays, and deliberately offers no combined ready signal — because a consumer forced to write the conjunction has to think about which layers it is conjoining. And the property that matters most here is a cover, not an assertion: the divergent window must be shown to have been visited, or the safety property guarding it is passing vacuously.
Two further costs are worth carrying forward. A redundant PRECHARGE re-arms tRP, so defensive precharging is not free. And tRP and tRCD coinciding at some speed grades means a design that shares one register between them passes every check at that grade and fails at the next.
15. What Comes Next
This chapter and Chapter 14.1 have constrained the two ends of a row's life from the outside — when a column command may start, and when the next ACTIVATE may start.
Neither has said anything about when the PRECHARGE itself may be issued. Chapter 14.3 supplies that, and it produces the most counter-intuitive pairing in DDR timing: a row that has been successfully read, whose data is in hand, and which may not yet be closed. READ legal, PRECHARGE illegal, at the same instant, on the same bank — and the reason is a minimum duration rather than a minimum separation between two commands.
Continue learning
Related tutorials
- Related topic
tRCD — RAS-to-CAS Delay
An accepted ACTIVATE constrains the earliest column command to that bank. It is a minimum separation, it is bank-local, and it is published in nanoseconds — three facts that between them explain most tRCD bugs.
- Related topic
Why Timing Parameters Exist
A DDR command can be perfectly meaningful and target a bank in exactly the right state and still be illegal right now. That third refusal is what timing parameters are, and it is a different question from the first two.
- Related topic
Device Physics Behind Timing
The array's processes take absolute time and know nothing about any clock. A controller counts cycles. Converting between them is where a physical duration becomes a digital obligation — and where the same device needs more cycles the faster you run it.
- Related topic
Timing Constraints Catalogue
Four classes of timing constraint, the resource that owns each one, and the equation the whole subject reduces to: a command becomes legal at the maximum of every applicable deadline — never the minimum, and never the most recent.
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.
