DDR · Module 13
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.
Chapter 13.1 built one timing obligation and gave it five parts. Chapter 13.2 resolved its magnitude into a countable integer and fixed the convention for counting it.
A single obligation, though, is not the situation a controller is ever in. Chapter 13.1 §5 noted that a candidate command is routinely the target of several edges at once and deferred what to do about it. This chapter answers it, and the answer is the most important equation in the module:
A command becomes legal at the maximum of every applicable deadline.
Getting there needs two things first. You have to know what kinds of constraint exist, because they do not all mean the same thing. And you have to know which resource owns each one, because attaching an obligation to the wrong resource produces two distinct hardware bugs that are exact opposites of each other.
1. What a Catalogue of Constraints Should Contain
There is an obvious way to write this chapter and it is the wrong one. The obvious way is a table: parameter name, what it constrains, typical value, repeat fifteen times.
That table is genuinely useful and it is Module 14. The reason not to write it here is that a list of parameters does not tell you how to use a list of parameters. You can know fifteen names and still not be able to answer whether a particular command is legal at a particular cycle, because that answer depends on which constraints apply, what they are attached to, and how they combine — none of which is in the table.
So this catalogue enumerates something else: the classes of constraint, and the scopes they attach to. There are only a few of each, they are stable across generations, and every named parameter you will ever meet is an instance of one class at one scope. Learn the small set and each new parameter becomes a two-question classification rather than a new fact.
2. Four Classes of Constraint
Chapter 13.1 §4 listed sense as the fifth part of an obligation — whether the requirement is a minimum or a fixed latency. That distinction is real and it is not a two-way split. There are four classes, and they differ in what they promise.
Class A — Minimum separation
Event B must not occur before some minimum time after event A.
A ──────── at least D ────────▶ B legal from here onwardThe device promises nothing about what happens if B occurs early, and nothing constrains B from being late. Waiting longer is always permitted. This is the most common class by far, and every obligation discussed so far has been one.
The defining property: a minimum separation has a lower bound and no upper bound.
Class B — Fixed or programmed latency
Given that A occurred, B will occur at a defined offset.
A ──────── exactly L ────────▶ B occurs hereThis is not a permission, it is a prediction — and it is a two-sided commitment. The device will deliver at the offset; the controller must be ready at the offset. Being late is as much a failure as being early, which is what makes this class different in kind.
Chapter 10.2 developed this for read data and Chapter 11.2 for write data. The offset is configured, not minimum: a controller that sets the configuration to one value and expects data at another is broken even though no separation was violated.
Class C — Resource occupancy
A resource is unavailable from one time until another.
A ──▶ │████ resource busy ████│ ──▶ availableFormally this is expressible as a minimum separation, and in RTL it usually is. Keeping it separate is worth it for two reasons.
It is attached to the resource, not to a command pair. An occupancy window blocks every command that needs that resource, including classes of command nobody thought about when the obligation was written. That is a different mental model from a pairwise edge, and it is the safer one when the set of competing commands grows.
It has a natural end as well as a start. Chapter 6.5's cas_to_data_window and Chapter 12.1's transfer counting both describe the data bus being occupied for a span, and a span is the natural object when you are asking whether two spans overlap.
Class D — Rolling-window rate limit
No more than N events of this kind may occur in any window of W time.
│◀──────── any window of W ────────▶│
│ at most N events in here │This class is genuinely different from the other three, and the difference is worth stating precisely: legality depends on several recent events rather than on one. Classes A, B and C each relate a candidate to one prior event. A rolling window relates it to a history.
The consequences for implementation are substantial. You cannot represent a rolling-window constraint with a single timer, because a timer cannot answer “how many events happened in the last W cycles.” You need either a count with expiry times, or a shift register of event times — and the storage is proportional to N, not to W.
DDR does impose a constraint of this class — a limit on how many row activations may occur within a window — and Module 14 owns it by name, along with why it exists and what it costs. It is named here only so the class has a real instance and so you know the class is not hypothetical.
| Class | Relates candidate to | Bound | Storage needed |
|---|---|---|---|
| A — minimum separation | one prior event | lower only | one deadline |
| B — fixed latency | one prior event | both sides | one offset, plus a pipeline |
| C — resource occupancy | one span | span end | one deadline per resource |
| D — rolling window | a history of events | count per window | proportional to the count |
3. Deriving the Rule for Several Constraints
Now the central derivation. It is short, which is part of the point — the equation is not deep, and treating it as a formula to memorise rather than to derive is exactly how people get it backwards.
Set up the situation with the tools already built. A candidate command is constrained by obligations 1 through k. From Chapter 13.2 §6, obligation i has a triggering event at cycle E_i and a magnitude D_i, and the candidate at cycle M satisfies obligation i exactly when:
M - E_i >= D_i ⟺ M >= E_i + D_iGive that right-hand side a name, because naming it is most of the insight:
deadline_i = E_i + D_i the cycle from which obligation i permits the candidateFrom Chapter 13.1 §2, timing legality is a conjunction — every applicable obligation must be satisfied, not some of them. So:
M is timing-legal ⟺ M >= deadline_1 AND M >= deadline_2 AND … AND M >= deadline_kA value is at least as large as every member of a set exactly when it is at least as large as the largest member. Therefore:
M is timing-legal ⟺ M >= max( deadline_1 , … , deadline_k )And the earliest legal cycle is the smallest such M, which is the maximum itself:
┌──────────────────────────────────────────────────────────┐
│ earliest_legal(candidate) = max over all applicable │
│ deadlines │
└──────────────────────────────────────────────────────────┘That is the equation the module has been heading toward, and the derivation makes clear why it is a maximum and not anything else: because the constraints are conjunctive, and a conjunction of lower bounds is dominated by the largest.
And now the property that makes this a live computation rather than a one-off calculation. Every issued command is a triggering event, so issuing arms new obligations and moves future deadlines. The maximum is therefore not a number you compute once — it is a quantity that changes as the controller acts. Chapter 13.1 §5 called this the third consequence of the graph view; here it is the reason the RTL in §7 recomputes its reduction every cycle rather than latching a result.
4. Scope — What a Deadline Is Attached To
Everything in §3 assumed we knew which obligations were applicable to a candidate. That word is carrying a lot of weight, and unpacking it is where the engineering is.
An obligation is applicable to a candidate when the candidate needs the resource the obligation is attached to. So applicability is determined by scope, and DDR obligations have several distinct scopes.
Bank-local. The obligation constrains commands to one bank and says nothing about other banks. An obligation arising from a row being opened in bank 3 concerns bank 3. Banks are substantially independent — that independence is the whole reason DDR devices have many of them — so a bank-local obligation must not restrain the others.
Shared data path. The obligation constrains the use of the data bus, which every bank shares. Chapter 12.1 showed a single column command occupying the data path for many transfer positions; a second column command whose data would overlap is illegal regardless of which bank it targets.
Shared command path. The command interface carries one command per cycle. Obligations here constrain the rate of commanding, independent of target.
Grouped. Some generations organise banks into groups that share some internal resource, so that two accesses to banks in the same group require more separation than two accesses to banks in different groups. This scope genuinely exists and it is generation-specific in both its presence and its details. Module 16 owns the architecture and Module 14 owns the parameters; this chapter needs only the observation that a scope can be coarser than a bank and finer than the whole device, which is enough to make the taxonomy complete.
The two opposite bugs
Scope is not bookkeeping. Getting it wrong produces one of two hardware bugs, and they are exact opposites. This is the most practically valuable material in the chapter.
Bug 1 — a bank-local obligation held globally. One timer, shared by all banks, armed whenever any bank triggers the obligation.
This is safe and slow. Every bank is blocked by every other bank's obligation, so the banks stop being independent. A workload spread across eight banks runs as though it had one. Nothing fails, no assertion fires, and the design meets every timing requirement — it simply throws away the parallelism the device was built to provide. Bugs of this shape survive to production regularly, because the only symptom is a number in a performance report that somebody attributes to the workload.
Bug 2 — a shared obligation held per bank. One timer per bank, for an obligation that actually constrains a shared resource.
This is fast and wrong. Bank 2's timer expires, bank 2's command issues, and the shared resource is still occupied by bank 5's activity — which bank 2's timer knew nothing about. This is a genuine contract violation, and its signature is nasty: it appears only when the traffic pattern spreads across banks, so a single-bank test passes perfectly and the failure rate rises with the very parallelism the design was built for.
| Bank-local obligation | Shared obligation | |
|---|---|---|
| Held per bank | ✓ correct | ✗ fast and wrong — illegal overlap across banks |
| Held globally | ✗ safe and slow — banks over-serialised | ✓ correct |
5. The Event-to-Event View
Putting classes and scope together gives the structure Chapter 13.1 §5 promised: a matrix over event classes, where each populated cell is an edge with a class, a scope and a magnitude.
Here is that matrix with abstract constraints, because the point is the structure and Module 14 owns the contents:
── constrained event ──
prior event ACTIVATE column cmd PRECHARGE
─────────────────────────────────────────────────────────
ACTIVATE α (Δ) β (B) γ (B)
column cmd · δ (S) ε (B)
PRECHARGE ζ (B) · ·
scope: (B) bank-local (S) shared path (Δ) grouped
· = no direct edge in this abstract modelThese are abstract labels, not parameters. α through ζ have no values here and no JEDEC standing. What the matrix demonstrates is structural and does generalise:
Most cells are populated, but not all. A precharge does not constrain a future column command directly — it constrains a future activate, which then constrains the column command. Some relationships are compositions of edges rather than edges.
Cells have different scopes. β is bank-local while δ is shared. Two edges in the same matrix, two completely different storage requirements, and §4's table says that confusing them gives you one of two opposite bugs.
A single candidate is hit by a whole column. The column-command column contains β and δ, so any candidate column command is subject to at least two edges — which is precisely §3's situation and precisely why the maximum rule exists.
Some edges are the same class at different scopes. α and β are both triggered by activation; one is grouped and one is bank-local. Module 14 will show that DDR really does have pairs of parameters distinguished only by scope, and the matrix is where that stops seeming arbitrary.
6. Two Ways to Hold a Deadline
The equation in §3 is arithmetic on cycle numbers. Hardware has to store something, and there are two established representations with genuinely different trade-offs.
Representation 1 — countdown
Store a remaining count per obligation. Load it with the magnitude when the obligation is armed, decrement every cycle, and the obligation is discharged at zero.
arm with D=4: 4 → 3 → 2 → 1 → 0
└─ legal from hereAdvantages. Intuitive and local. The stored value answers “how much longer” directly, which makes it self-documenting in a waveform. It needs only ceil(log2(D_max + 1)) bits. And it cannot wrap in any dangerous way, because it stops at zero.
Costs. Every outstanding obligation needs a decrement every cycle, so activity is proportional to the number of obligations. Comparing two countdowns to find the larger works, but the numbers are remaining times rather than absolute cycles, so composing them with anything expressed in absolute terms needs conversion.
This is the corpus's existing style: Chapter 11.4's write_recovery_guard and Chapter 4.5's bank_group_spacer both count down.
Representation 2 — absolute deadline
Keep one free-running cycle counter. When an obligation is armed, store the timestamp at which it expires: now + D. The obligation is discharged when now reaches the stored deadline.
now = 21, arm with D=4: store deadline = 25
discharged when now >= 25Advantages. No per-obligation activity — only the single shared counter advances. Deadlines are directly comparable, which makes §3's maximum a plain reduction over stored values with no conversion. And “when does this become legal” is answerable now, without waiting, which matters for a scheduler looking ahead.
Costs. The timestamp must be wide enough, and more importantly the counter wraps. Once it wraps, now >= deadline as a plain unsigned comparison is simply wrong, and the standard fix is not obvious. §7 implements it and §11 covers the failure it prevents.
7. RTL — A Deadline Scoreboard
Collision check. Chapter 11.4's write_recovery_guard is one countdown obligation. Chapter 4.5's bank_group_spacer is two countdowns with scope and a blocked-reason output. Chapter 2.3's refresh_deadline_tracker tracks a recurring obligation with an owed count. Chapter 13.2's resolver computes a magnitude and holds none.
What none of them does is reduce several concurrent obligations to a maximum and report which one is controlling. That, plus the timestamp representation and its wraparound discipline, is this block's territory. It is deliberately not a better bank_group_spacer: that block arbitrates and issues, and this one computes and diagnoses.
The engineering problem. Hold k concurrent obligations, each armed at a different time with a different magnitude. Answer two questions every cycle: are they all discharged? and if not, which one will be last, and when? The second question is what makes this more than a pile of timers — Chapter 13.1 §14's fourth exercise showed that reporting an outstanding obligation is easy and nearly useless, while reporting the controlling one requires magnitudes.
Classification: controller-side sequential timing bookkeeping.
What it models: cycle accounting. A free-running counter, stored expiry timestamps, a wrap-tolerant comparison, and a maximum reduction.
What it does not model: anything physical. There is no representation here of sensing, restoration, charge or any analog process. The magnitudes arrive as inputs from Chapter 13.2's resolver, which got them from a datasheet. This block would behave identically if the obligations described a disk drive. That indifference is the correct relationship between a controller and device physics, and it is worth noticing that the RTL makes it structurally obvious.
// ─────────────────────────────────────────────────────────────────────
// deadline_scoreboard
//
// CLASSIFICATION
// Controller-side sequential timing bookkeeping. Counts cycles and
// compares numbers. Nothing else.
//
// WHAT IT MODELS
// §3's rule, in the §6 timestamp representation:
// deadline_i = arm_cycle_i + magnitude_i
// earliest_legal = max over outstanding deadline_i
// plus identification of the CONTROLLING obligation -- the one whose
// deadline is the maximum -- because that is the one a debugger or a
// scheduler actually needs to know about.
//
// WHAT IT DOES NOT MODEL
// Any physical process. Any analog behaviour. Any device internals.
// Magnitudes are inputs; this block has no idea what they describe.
//
// CONVENTION (from Chapter 13.2 §6, obeyed exactly)
// An obligation armed while the counter reads N, with magnitude D,
// permits its constrained command from cycle N + D onward. D == 0
// therefore means "same cycle is legal" and arms nothing.
//
// SIMULTANEITY RULES (specified, asserted in §9, tested in §10)
// arm and expiry on the same cycle -> ARM WINS. The slot re-arms
// from the new event. An obligation must never be discharged by
// an expiry that a fresh triggering event has superseded.
// arm while already outstanding -> OVERWRITE with the new
// deadline, even if that is EARLIER. See the note below: this
// is a deliberate choice and it is not always the right one.
// reset mid-obligation -> all obligations abandoned.
// Correct here because reset means the controller has lost its
// history and must not pretend otherwise.
//
// WRAPAROUND -- the point of the whole block
// `now` is free-running and WILL wrap. A plain unsigned
// `now >= deadline` is wrong across a wrap boundary: a deadline of
// 3 with `now` at 250 compares as not-yet-reached when in fact it
// passed long ago. The fix is to compare the SIGNED DIFFERENCE:
// reached <=> $signed(now - deadline) >= 0
// which is correct as long as the true gap never exceeds half the
// counter range. The elaboration guard below enforces exactly that,
// by bounding every magnitude below half of 2**TS_W.
// ─────────────────────────────────────────────────────────────────────
module deadline_scoreboard #(
// Concurrent obligations tracked. §3's k.
parameter int SLOTS = 4,
// Width of the free-running cycle counter and of every timestamp.
parameter int TS_W = 8,
// Width of a magnitude. MUST leave the signed-difference comparison
// valid; the guard below is the real specification.
parameter int MAG_W = 5,
// Index into the slots, guarded so SLOTS == 1 stays legal.
parameter int SLOT_W = (SLOTS <= 1) ? 1 : $clog2(SLOTS)
) (
input logic clk,
input logic rst_n,
// ── One bit per slot: a triggering event for that obligation.
input logic [SLOTS-1:0] arm,
// ── Per-slot magnitude, in cycles, sampled when that slot is armed.
// Supplied by Chapter 13.2's resolver in a real design.
input logic [SLOTS-1:0][MAG_W-1:0] magnitude,
// ── The free-running counter. Published because every consumer and
// every checker needs the same notion of "now".
output logic [TS_W-1:0] now,
// ── Timing legality for a candidate subject to ALL these slots.
// This is Chapter 13.1's timing_ok, computed rather than assumed.
output logic all_clear,
// ── §3's maximum: the cycle from which every obligation permits the
// candidate. Meaningful only while controlling_valid is high.
output logic [TS_W-1:0] earliest_legal,
// ── WHICH obligation is controlling -- the one holding the maximum.
// This is the output that makes the block worth building.
output logic [SLOT_W-1:0] controlling_slot,
output logic controlling_valid,
// ── How much longer the controlling obligation has to run. One bit
// wider than a magnitude so it can represent a full magnitude
// without truncation.
output logic [MAG_W:0] cycles_remaining
);
// ── Elaboration guards.
if (SLOTS < 1) begin : g_slots
initial $fatal(1, "deadline_scoreboard: SLOTS must be >= 1");
end
if (TS_W < 2) begin : g_ts_w
initial $fatal(1, "deadline_scoreboard: TS_W must be >= 2");
end
if (MAG_W < 1) begin : g_mag_w
initial $fatal(1, "deadline_scoreboard: MAG_W must be >= 1");
end
// ── THE guard that makes the signed comparison sound. The largest
// representable magnitude is 2**MAG_W - 1, and the signed
// difference is only trustworthy for gaps below 2**(TS_W-1).
// Requiring MAG_W <= TS_W - 1 gives 2**MAG_W - 1 < 2**(TS_W-1),
// with a full cycle of headroom. Without this guard the block is
// subtly wrong in a way that only appears after a wrap, which is
// the hardest class of bug to find in the field.
if (MAG_W > (TS_W - 1)) begin : g_horizon
initial $fatal(1, "deadline_scoreboard: need MAG_W <= TS_W-1 for wrap safety");
end
// ── State: one deadline timestamp and one outstanding flag per slot.
logic [TS_W-1:0] deadline [SLOTS];
logic [SLOTS-1:0] outstanding;
// ── Combinational view of which outstanding slots have expired.
logic [SLOTS-1:0] reached;
always_comb begin
for (int unsigned i = 0; i < SLOTS; i++) begin
// The wrap-tolerant comparison. Both operands are TS_W wide, so
// the subtraction is TS_W wide and wraps; reinterpreting it as
// signed asks "is the deadline in the past or now?", which is
// the question that survives a counter wrap.
reached[i] = ($signed(now - deadline[i]) >= 0);
end
end
// ── The reduction. Recomputed every cycle, never latched, because
// §3's closing observation is that issuing changes the set.
//
// "Maximum" needs care under wraparound too: the largest
// TIMESTAMP is not the furthest-future deadline once the counter
// has wrapped. So the comparison is on the signed REMAINING time,
// which is wrap-correct for the same reason `reached` is.
logic signed [TS_W-1:0] remaining_i;
int signed best_remaining;
always_comb begin
all_clear = 1'b1;
earliest_legal = now;
controlling_slot = '0;
controlling_valid = 1'b0;
cycles_remaining = '0;
best_remaining = -1;
for (int unsigned i = 0; i < SLOTS; i++) begin
if (outstanding[i] && !reached[i]) begin
all_clear = 1'b0;
remaining_i = $signed(deadline[i] - now);
// Strictly greater, so the LOWEST index wins a tie. Fixed
// tie-break, so a report is reproducible run to run.
if (int'(remaining_i) > best_remaining) begin
best_remaining = int'(remaining_i);
earliest_legal = deadline[i];
controlling_slot = SLOT_W'(i);
controlling_valid = 1'b1;
// best_remaining is bounded above by the largest magnitude,
// which the g_horizon guard keeps within MAG_W bits, so this
// cast into MAG_W+1 bits cannot truncate.
cycles_remaining = (MAG_W+1)'(remaining_i);
end
end
end
end
// ── Sequential state.
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
now <= '0;
outstanding <= '0;
for (int unsigned i = 0; i < SLOTS; i++) begin
deadline[i] <= '0;
end
end else begin
now <= now + 1'b1;
for (int unsigned i = 0; i < SLOTS; i++) begin
if (arm[i]) begin
// ARM WINS over a simultaneous expiry -- see the header.
deadline[i] <= now + TS_W'(magnitude[i]);
// A magnitude of zero permits the candidate on this very
// cycle, so there is nothing to hold outstanding.
outstanding[i] <= (magnitude[i] != '0);
end else if (outstanding[i] && reached[i]) begin
outstanding[i] <= 1'b0;
end
end
end
end
endmoduleInterface contract. arm and magnitude are sampled together; a magnitude is meaningless except on the cycle its slot is armed. earliest_legal, controlling_slot and cycles_remaining are valid only while controlling_valid is high — with everything discharged there is no controlling obligation and earliest_legal degenerates to now, which a consumer must not read as a deadline. all_clear is the one output that is always meaningful, and it is exactly Chapter 13.1's timing_ok for a candidate subject to all these slots.
Parameter contract. The g_horizon guard is the important one and it deserves restating: MAG_W <= TS_W - 1. It is what makes the signed comparison sound, and it is enforced at elaboration rather than documented as a caution, because a wrap bug that only manifests after 2**TS_W cycles of uptime is not something you want guarded by a comment.
Why the casts are safe. SLOT_W'(i) narrows a loop variable bounded by SLOTS - 1, and SLOT_W is $clog2(SLOTS) — wide enough for SLOTS - 1, which is the value that occurs, and not wide enough for SLOTS, which does not. (MAG_W+1)'(remaining_i) holds a value bounded by the largest magnitude, and g_horizon bounds magnitudes within MAG_W bits, so MAG_W + 1 bits is one bit of slack. TS_W'(magnitude[i]) widens rather than narrows, since g_horizon forces MAG_W < TS_W.
Synthesis implications. One shared counter, SLOTS timestamp registers, SLOTS subtractors for the comparison and a SLOTS-way maximum tree. The tree is the only part with depth that matters, and it is logarithmic in SLOTS. Compare against SLOTS independent countdowns: the same register count, similar logic, but no decrement activity, which is the switching-power argument from §6.
Corner cases. SLOTS == 1: SLOT_W is 1, the reduction is trivial, the block still works. magnitude == 0: arms nothing, per the convention — and note this is distinguishable from “not armed” only by watching arm, which is the ambiguity Chapter 13.2 §12 warned about. Every slot outstanding with identical deadlines: the lowest index is reported, deterministically. Reset while obligations are outstanding: all abandoned, which is correct because a reset controller has no history to honour.
Failure modes. Using unsigned now >= deadline[i] instead of the signed difference gives a block that is perfect for 2**TS_W cycles and then reports obligations as outstanding for almost a full counter period — at 1600 MHz with TS_W of 8, that is a failure every 160 nanoseconds, but with TS_W of 32 it is a failure roughly every three seconds, which is exactly long enough to pass every test and fail in the field. Taking the minimum rather than the maximum in the reduction reports legality too early. Latching the reduction instead of recomputing it misses obligations armed after the latch. Letting expiry win over a simultaneous arm discharges an obligation that a fresh event just renewed.
8. Two Obligations, One Maximum
A trace with two obligations armed at different times with different magnitudes. It shows the controlling obligation changing hands and the maximum being what matters.
deadline_scoreboard — the later deadline governs
10 cyclesCycle 4 is the instructive moment. Slot 0's obligation is fully discharged. A design that concluded legality from “an obligation expired” would issue here, four cycles early. Nothing in slot 0's own state is wrong — it correctly reports itself discharged. The error would be in the reduction, which is why §3's derivation is about the reduction and not about any individual obligation.
Note also that recency and ordering come apart. Slot 1 was armed after slot 0 and expires after it. Reverse the magnitudes — slot 0 with 6 and slot 1 with 3 — and the obligation armed second would expire first, with slot 0 controlling throughout. The controlling obligation is determined by deadline, never by arming order, which is the second wrong reading in §3's callout and one that a single trace can easily fail to expose.
9. Five Assertions Worth Writing
// ── P1. all_clear means every obligation is genuinely discharged.
// Catches the reduction-OR / minimum bug in its most direct form: a
// design that clears while an obligation is outstanding.
property p_all_clear_is_conjunctive;
@(posedge clk) disable iff (!rst_n)
all_clear == ((outstanding & ~reached) == '0);
endproperty
a_all_clear_is_conjunctive: assert property (p_all_clear_is_conjunctive);
// ── P2. The reported deadline is the MAXIMUM, not merely one of them.
// Written as the two halves of "is the maximum": it is one of the
// outstanding deadlines, AND no outstanding deadline is further in
// the future. The second half is the one that catches a minimum
// reduction, and it is the half people forget to write.
property p_earliest_legal_is_the_maximum;
@(posedge clk) disable iff (!rst_n)
controlling_valid |->
( (earliest_legal == deadline[controlling_slot])
&& (outstanding[controlling_slot] && !reached[controlling_slot]) );
endproperty
a_earliest_legal_is_the_maximum: assert property (p_earliest_legal_is_the_maximum);
// ── P2b. The other half of maximality, over every slot. A generate
// loop because a procedural for-loop is ILLEGAL inside a property;
// the genvar unrolls it into SLOTS separate properties instead.
generate
for (genvar gi = 0; gi < SLOTS; gi++) begin : g_max_dominates
property p_no_slot_exceeds_the_reported_maximum;
@(posedge clk) disable iff (!rst_n)
(controlling_valid && outstanding[gi] && !reached[gi]) |->
($signed(deadline[gi] - now) <= $signed(earliest_legal - now));
endproperty
a_no_slot_exceeds: assert property (p_no_slot_exceeds_the_reported_maximum);
end
endgenerate
// ── P3. An armed slot's deadline honours Chapter 13.2 §6's convention
// exactly: arm at N with magnitude D gives deadline N + D. Catches
// the off-by-one that §11 of the previous chapter is entirely about,
// at the one place it can be checked arithmetically.
// $past with an explicit 1 -- never 0, which is illegal.
generate
for (genvar gj = 0; gj < SLOTS; gj++) begin : g_arm_convention
property p_deadline_follows_convention;
@(posedge clk) disable iff (!rst_n)
$past(arm[gj], 1) |->
(deadline[gj] == ($past(now, 1) + TS_W'($past(magnitude[gj], 1))));
endproperty
a_deadline_follows_convention: assert property (p_deadline_follows_convention);
end
endgenerate
// ── P4. Arm wins over a simultaneous expiry. The documented
// simultaneity rule, asserted so it cannot silently change.
// Catches the ordering bug where an obligation renewed on the very
// cycle its predecessor expired is discharged immediately.
generate
for (genvar gk = 0; gk < SLOTS; gk++) begin : g_arm_priority
property p_arm_beats_expiry;
@(posedge clk) disable iff (!rst_n)
($past(arm[gk], 1) && ($past(magnitude[gk], 1) != '0))
|-> outstanding[gk];
endproperty
a_arm_beats_expiry: assert property (p_arm_beats_expiry);
end
endgenerate
// ── P5. A discharged obligation stays discharged unless re-armed.
// Catches the wraparound bug directly. With an unsigned comparison
// this property holds for 2**TS_W cycles and then fails, because a
// long-passed deadline starts comparing as not-yet-reached again.
// Run it past a wrap boundary or it proves nothing -- see §10.
generate
for (genvar gm = 0; gm < SLOTS; gm++) begin : g_no_resurrection
property p_no_obligation_resurrects;
@(posedge clk) disable iff (!rst_n)
(!outstanding[gm] && !arm[gm]) |=> !outstanding[gm];
endproperty
a_no_obligation_resurrects: assert property (p_no_obligation_resurrects);
end
endgenerateWhat these prove. That all_clear is conjunctive; that the reported deadline is genuinely maximal in both directions; that the arming convention matches Chapter 13.2 §6 exactly; that the documented simultaneity rule holds; and that obligations do not resurrect, which is the wraparound property.
What these do not prove. Three real gaps.
Nothing here proves the magnitudes are correct. They are inputs. A scoreboard fed magnitudes that are all one cycle short satisfies every property and violates the device's contract on every command. That check belongs where the magnitudes are produced — Chapter 13.2 §9 — and at the system level, which is Chapter 13.4.
Nothing here proves the right obligations were armed. If a bank-local obligation is wired to a slot that is armed by every bank's activity, §4's Bug 1 is present, every property passes, and the design is merely slow. Scope errors are invisible to block-level assertions, because the block cannot see what its slots are supposed to mean. That is the single most important limitation in this chapter's verification story.
P5 proves nothing unless the simulation crosses a wrap boundary. With TS_W of 32 that is four billion cycles, which no test will run. §10 explains what to do about that, and the answer is not to hope.
10. Verification Perspective
Shrink the counter to reach the wrap. This is the most important testing idea in the chapter. The wraparound bug is real, it is severe, and with a production-width counter it is unreachable by simulation. So parameterise TS_W down — to 4 or 5 bits — and run thousands of cycles. The counter wraps every 16 or 32 cycles, P5 gets exercised hundreds of times, and a design using an unsigned comparison fails almost immediately.
This works because the wraparound correctness argument does not depend on TS_W: the signed comparison is sound whenever magnitudes stay below half the range, which g_horizon enforces at every width. So a small-width proof is a proof at all widths, and that is what makes the technique sound rather than merely convenient. Then run the production width for everything else.
Test the simultaneity rules individually. Arm on the exact expiry cycle and confirm the slot stays outstanding (P4). Arm two slots on the same cycle. Arm and reset on the same cycle. Arm a slot that is already outstanding, both with a later deadline and with an earlier one — and check which variant §7's callout says you implemented. The earlier-deadline case is the one that distinguishes the two variants, and a suite that only ever re-arms with later deadlines cannot tell them apart.
Drive the degenerate magnitude. magnitude == 0 must arm nothing. Confirm the slot never goes outstanding and that all_clear is unaffected. Then check the consumer's behaviour, because zero is where Chapter 13.2 §12's last misconception lives.
Check maximality with an independent model, and invert the representation. Keep a behavioural model that holds countdowns rather than timestamps, decrements them, and reports the largest remaining. Compare its answer with the RTL's every cycle. This is deliberate: §6 argued the two representations are equivalent, and a checker in the opposite representation cannot share the RTL's wraparound bugs because it has no timestamps to wrap. Chapter 13.4 §9 builds this properly.
Randomise arming, then constrain toward the interesting cases. Purely random arming rarely produces the case that matters — several slots outstanding with well-separated deadlines and the maximum changing hands. Constrain magnitudes to a spread and arm slots at staggered intervals. Cover the event “controlling_slot changed while more than one slot was outstanding,” because that is §8's cycle-2 transition and the reduction bug lives there.
Cover, do not assume. Every generated property above is an implication. Cover arm, cover controlling_valid, cover multi-slot outstanding, and cover the wrap. A suite reporting five passing properties on a run where only one slot was ever armed has proved almost nothing.
11. Debugging — Scope Errors and the Bug That Waits for a Wrap
Two symptom families, both specific to this chapter's material.
Symptom A — banks that are not independent
Bank 1's commands stall while bank 0 is busy, and no shared resource is involved.
| Candidate mechanism | Evidence | Discriminator |
|---|---|---|
| Bank-local obligation held globally — §4's Bug 1 | One slot arms on any bank's activity | Look at what arms the slot. If activity in bank 0 arms the obligation that blocks bank 1, this is it. |
| A genuinely shared obligation | The stall correlates with data-bus or command-bus use | Trace whether the resource is genuinely shared. Sometimes the answer is that the banks are correctly coupled. |
| Grouped scope, correctly implemented | Stalls occur between specific bank pairs, not all pairs | Check the grouping. Banks in the same group are supposed to constrain each other more. |
| The scheduler, not the constraints | all_clear is high and the command still does not issue | Not a timing problem at all. Chapter 13.1 §11's last row: check whether the candidate was presented. |
The decisive test is a one-bank workload against an all-banks workload. If aggregate throughput does not improve with bank parallelism, obligations are coupling banks that should be independent. This comparison costs one simulation and settles the question, which is why it is worth running early rather than after a week of staring at waveforms.
Symptom B — a failure that only appears after long uptime
Everything works. After some hours, or in a long soak test, obligations begin reporting as outstanding when they are not, and throughput collapses for a while before recovering.
This is the wraparound bug, and its periodicity is the fingerprint. With TS_W of 32 at 1600 MHz, the counter wraps roughly every 2.7 seconds — so the symptom recurs on a fixed period unrelated to anything in the workload.
| Candidate mechanism | Evidence | Discriminator |
|---|---|---|
| Unsigned comparison across a wrap | Symptom period matches 2**TS_W × tCK | Compute the period. If it matches, this is almost certainly it. |
| Timestamp too narrow for a magnitude | Failures correlate with the largest magnitudes | g_horizon. If the guard is absent, check MAG_W against TS_W - 1. |
| A max reduction on raw timestamps | Wrong controlling slot reported near a wrap, briefly | The reduction must compare remaining time, not raw timestamps. §7's note. |
| Something else entirely | Period does not match | Do not force the diagnosis. Wrap bugs have arithmetic periods; workload effects do not. |
Responsible layer is the comparison, not the counter. Widening the counter is the instinctive fix and it is the wrong one: it makes the period longer and the bug harder to find while leaving it present. A wrap bug is fixed by fixing the comparison, and the way to confirm the fix is §10's shrink-the-counter technique, which converts a three-second bug into a thirty-cycle one.
12. Common Misconceptions
“Once one timer expires, the command is legal.” Tempting because an expiring timer is a satisfying event and it genuinely did satisfy something. Why it is wrong: it satisfied one conjunct. §3's derivation gives a maximum, so the command is legal when the last obligation clears. Consequence: the command issues early by the spread of the deadlines — a contract violation whose size depends on traffic and which therefore looks non-deterministic. Replacement model: legality is a conjunction; the maximum dominates. Debugging clue: violations scale with how many obligations are typically outstanding, so they worsen as traffic gets more varied.
“The shortest deadline controls.” Tempting because “the binding constraint” sounds like the tightest one, and in many optimisation problems it is. Why it is wrong: these are lower bounds on a single quantity, so the largest binds. The shortest deadline is the one that stops mattering first. Consequence: the same early issue as above. Replacement model: for lower bounds, the maximum binds. Debugging clue: the reported controlling obligation is always the one that expires soonest, which is exactly backwards.
“Every timing rule is per bank.” Tempting because banks are the unit DDR discussions revolve around and many obligations genuinely are bank-local. Why it is wrong: the data path and the command path are shared by every bank. Consequence: §4's Bug 2 — fast and wrong, with a failure rate that rises with bank parallelism, so the more the design succeeds at its goal the more it fails. Replacement model: attach the deadline to the resource whose reuse it constrains. Debugging clue: single-bank tests pass; multi-bank tests fail.
“Every timing rule is global.” Tempting because one timer is simpler and obviously safe. Why it is wrong: it couples independent banks. Consequence: §4's Bug 1 — safe and slow, invisible to every assertion, and frequently shipped. Replacement model: the same rule, applied in the other direction. Debugging clue: throughput does not improve with bank parallelism.
“One global timer is simpler and therefore correct.” Tempting because conservatism is usually safe in timing, and here it is safe. Why it is wrong: the conclusion smuggles in “therefore good.” A safe design that discards the device's parallelism has failed at its job, and the failure does not announce itself. Consequence: a controller that meets every requirement and delivers a fraction of achievable bandwidth. Replacement model: safety and correctness are not the same property as fitness for purpose. Debugging clue: the design passes everything and the performance number is disappointing in a way nobody can explain.
“A per-bank timer is always more parallel.” Tempting because it is the mirror of the previous point, and per-bank state does enable parallelism. Why it is wrong: for a shared obligation, per-bank timers are not more parallel — they are wrong, and the apparent parallelism is the violation. Consequence: Bug 2 again, now arrived at by reasoning about performance rather than correctness, which is how it usually happens. Replacement model: parallelism is a consequence of correct scoping, not a goal to pursue independently. Debugging clue: a performance improvement that arrived with no corresponding change in the constraint set deserves suspicion.
“A legal command stays legal.” Tempting because deadlines pass and never un-pass. Why it is wrong: issuing other commands arms new obligations, some of which constrain this candidate. Consequence: a design that computes legality and then issues something else first will issue an illegal command. Replacement model: legality is evaluated at an instant against current history, which is why §7's reduction is recomputed every cycle. Debugging clue: the violating command is always preceded by a different command on a shared resource.
“An absolute-deadline implementation is just a countdown with extra steps.” Tempting because §6 says they are equivalent, and for a single obligation they behave identically. Why it is wrong: the timestamp form introduces a failure mode the countdown does not have — the counter wraps — and removes one the countdown does have, per-obligation switching activity. They are equivalent in what they compute and different in how they fail. Consequence: a team that switches representation for the power saving and does not add the signed comparison ships §11's Symptom B. Replacement model: the representations have different failure modes; choosing one means accepting its. Debugging clue: a bug with an arithmetic period is a representation bug, not a logic bug.
13. Interview Reasoning
“Three constraints give deadlines 12, 15 and 14. When is the command first legal, and why?” Cycle 15, and the reason matters more than the number. Each constraint is a lower bound on the issue cycle, legality is the conjunction of all three, and a value satisfying every lower bound must be at least the largest — so the earliest legal cycle is the maximum. Volunteering why the alternatives fail is the strong answer: 12 satisfies only one constraint, and summing the three serialises obligations that are running concurrently.
“What is the difference between a latency and a minimum spacing?” A minimum spacing is a one-sided permission — not before, no upper bound, and late is fine. A latency is a two-sided commitment: the event happens at the configured offset, and being ready late is a failure just as being early is. The consequence to reach for is that confusing them breaks in opposite directions — treating a latency as a minimum loses data because the capture path is not ready at the offset, while treating a minimum as a latency forfeits scheduling freedom for no benefit.
“Why can a single global timer reduce performance even though it is safe?” Because it applies one bank's obligation to every bank, so banks stop being independent and a workload spread across eight banks runs like a workload on one. The reason this is worth calling a bug rather than a trade-off is that it is invisible — every timing requirement is met, no assertion fires, and the only symptom is a performance number. The test is whether aggregate throughput improves with bank parallelism; if it does not, obligations are coupling banks that should be independent.
“Why can a per-bank timer be unsafe for a shared constraint?” Because each bank's timer knows only its own history, and the shared resource is occupied by other banks' activity. Bank 2's timer expires, bank 2 issues, and the data bus is still carrying bank 5's burst. The signature is the memorable part: single-bank tests pass and the failure rate rises with parallelism, so the bug gets worse as the design gets better at its job.
“How would you represent a timing constraint in RTL, and what are the choices?” Either a countdown per obligation — load the magnitude, decrement, legal at zero — or a single free-running counter with a stored expiry timestamp per obligation. Countdowns are simpler, local, and cannot wrap dangerously; timestamps have no per-obligation activity and make the maximum a plain comparison. The answer that demonstrates real understanding names the cost: timestamps wrap, so the comparison must be on the signed difference rather than unsigned magnitude, and the magnitude must stay below half the counter range for that to be sound.
“How can an absolute-deadline implementation fail after the counter wraps?”
An unsigned now >= deadline test breaks once the counter has wrapped past a stored deadline: a deadline of 3 with now at 250 reads as not-yet-reached, so a long-discharged obligation appears outstanding for nearly a full counter period. The fix is to compare the signed difference, valid while gaps stay under half the range. The part worth adding is how to find it — shrink the counter width in simulation so it wraps every few dozen cycles, which turns a bug that recurs every few seconds of uptime into one that recurs constantly, and the correctness argument is width-independent so the small-width test is a real proof.
“What information does a timing block need to publish for debugging?” Not just whether it is blocking — which obligation is controlling and how much longer it has. “Blocked” sends you looking at everything; “slot 2 controls, six cycles remaining” sends you to one triggering event. Reporting an arbitrary outstanding obligation is nearly as useless as reporting none, because the one you name may clear immediately while the candidate keeps waiting — which is why the reduction must compare magnitudes rather than merely testing bits.
14. Engineering Exercises
1. Compute an earliest legal cycle. A candidate column command to bank 2 is constrained by: obligation P, armed at cycle 10 with magnitude 6; obligation Q, armed at cycle 14 with magnitude 3; obligation R, armed at cycle 8 with magnitude 11. Compute each deadline, the earliest legal cycle, and the controlling obligation. Then say which obligation a naive “most recently armed” implementation would report, and how far off it would be.
Worked: deadlines are P at 16, Q at 17, R at 19. The maximum is 19, so the earliest legal cycle is 19 and R controls. A most-recently-armed implementation reports Q, whose deadline is 17 — issuing two cycles early, a real violation. Note that R was armed first and controls, which is §3's second wrong reading exactly: arming order and deadline order are unrelated.
2. Now change one event. In exercise 1, re-arm obligation P at cycle 15 with magnitude 2. Recompute. Did the earliest legal cycle move? Did the controlling obligation change? What does your answer say about caching a legality result?
Worked: P's deadline becomes 17, so deadlines are 17, 17, 19. The maximum is still 19 and R still controls, so the earliest legal cycle did not move. The lesson is that a re-arm may or may not change the answer, and you cannot know without recomputing the maximum — which is the argument against caching. Note also that P's new deadline is earlier than its old one, which is exactly the case §7's callout says distinguishes the overwrite variant from the extend-only variant, and here the difference is invisible because R dominates either way.
3. Classify five obligations by scope. For each, say bank-local, shared data path, shared command path, or grouped, and justify from the resource: (a) the interval after opening a row before that row may be closed; (b) the separation between two column commands whose data would occupy the data bus; (c) a limit on how many rows may be opened in a rolling window; (d) the separation between activations of two banks in the same group; (e) the interval after a write's data completes before that bank may be precharged.
Worked: (a) bank-local — the resource is that bank's row. (b) shared data path — it constrains the bus regardless of bank, and this is the classic Bug 2 candidate. (c) neither local nor shared in the simple sense: it is class D, a rolling window over the whole device, and it needs history rather than a deadline. (d) grouped, and generation-specific. (e) bank-local, and note it is triggered by data completion rather than by the command — which Chapter 11.4 makes a point of, and which is the five-part form's first element doing real work.
4. Repair a global timer. You inherit a controller with one timer for an obligation that is genuinely bank-local across eight banks. Describe the symptom you would measure, the change required, the cost in registers, and one new bug the change could introduce.
Worked: the symptom is that aggregate throughput does not scale with the number of banks in use — measure one-bank against eight-bank throughput. The change is to replicate the deadline register per bank and index it by the candidate's bank. The cost is eight timestamps instead of one, plus an eight-way reduction. The new bug: if any obligation currently sharing that timer is actually shared rather than bank-local, replicating it converts safe-and-slow into fast-and-wrong. Auditing which obligations the timer was carrying is the required first step, and skipping it is how a performance fix becomes a data-corruption bug.
5. Repair a per-bank timer. Now the mirror: an obligation constraining the shared data path is held per bank. Describe the symptom, why single-bank tests pass, the change, and how you would prove the fix.
Worked: the symptom is data corruption or protocol-checker violations that appear only with multi-bank traffic and worsen with more banks. Single-bank tests pass because with one bank in use, that bank's timer happens to hold the complete history of the shared resource — the per-bank state is accidentally correct. The change is to collapse the per-bank deadlines into one, armed by any use of the data path. Proving it needs a directed test: two banks issuing column commands as close together as their bank-local obligations allow, with a checker on data-bus overlap.
6. Write the maximality assertion. Without looking at §9, write an SVA property asserting that the reported earliest_legal is the maximum over outstanding deadlines. Then compare. Did you write both halves — that it is one of them, and that none exceeds it? Which half catches a minimum reduction? Why can the second half not be written with a procedural for loop inside the property?
7. Reach the wrap. Set TS_W to 4 and MAG_W to 3 and run 500 cycles with continuous arming. How many times does the counter wrap? Replace the signed comparison with an unsigned one and report which property fails first and after how many cycles. Then argue why this small-width experiment is a valid proof for TS_W of 32.
8. Choose a representation. A design has 64 concurrent obligations, mostly idle, and must report the controlling one every cycle. Argue for timestamps or countdowns, quantify the register and activity cost of each, and name the failure mode you are accepting.
9. Extend to a rolling window. §2's class D cannot be represented by one deadline. Sketch the state needed to enforce “at most N events in any window of W cycles” and say how it scales with N and with W. Which of the two does the storage depend on, and why is that initially surprising?
15. Summary
A timing catalogue worth having is a catalogue of classes and scopes, not values — because a list of parameters does not tell you how to use a list of parameters.
There are four classes. Minimum separation is one-sided: not before, and late is fine. Fixed latency is two-sided: the event occurs at the configured offset, and being ready late fails as surely as being early. Resource occupancy is a span attached to a resource rather than a pair of commands. Rolling window is different in kind, because legality depends on a history of events rather than on one, and its storage grows with the event count rather than the window.
Obligations attach to resources, and the rule that prevents both scope bugs is that a deadline belongs to the resource whose reuse it constrains — no finer, no coarser. Finer permits illegal overlap: fast and wrong, passing single-bank tests and failing more as parallelism grows. Coarser destroys independence: safe and slow, invisible to every assertion, and shipped more often than anyone would like.
The central result, derived rather than asserted: each obligation i contributes a deadline E_i + D_i, legality is the conjunction of all of them, and a value satisfying every lower bound must be at least the largest. So
earliest_legal(candidate) = max over all applicable deadlinesNot the minimum — that satisfies one constraint. Not the most recently armed — arming order and deadline order are unrelated. Not the sum — the obligations run concurrently. And because every issued command arms new obligations, the maximum is a live quantity to be recomputed, not a result to cache.
Deadlines can be held as countdowns or as absolute timestamps. They compute the same thing and fail differently: countdowns cost per-obligation activity, and timestamps wrap. A wrapping counter needs the signed-difference comparison and a magnitude bounded below half the counter range — a requirement worth enforcing at elaboration, because the bug it prevents has an arithmetic period measured in seconds of uptime and will not appear in any simulation that does not deliberately shrink the counter to find it.
deadline_scoreboard implements all of this and reports the controlling obligation, which is the output that turns “blocked” into a single triggering event to go and look at.
16. What Comes Next
The module now has a complete timing engine. Chapter 13.1 gave obligations their structure, Chapter 13.2 gave them magnitudes and a counting convention, and this chapter gave them classes, scopes and a composition rule.
What is missing is the procedure. Given a command trace, how does an engineer actually prove it is legal — by hand, at a whiteboard, or in a checker that does not simply mirror the controller's own arithmetic?
Chapter 13.4 supplies it: a repeatable eight-step analysis applied to a full worked trace, the discipline of building a timing checker whose representation is deliberately different from the design's so the two cannot share a bug, and a diagnostic report that names the controlling deadline, its source event and its resource rather than saying “timing error.”
That closes the module — and makes Module 14 the obvious next step, because at that point the framework is built and empty, and what it needs is the named DDR edges that populate it.
Continue learning
Related tutorials
- 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
Command Scheduling
An eight-step procedure for deciding when a command becomes temporally legal, worked through a full trace by hand — and a timing checker built in the opposite representation to the design, so the two cannot share a bug.
- 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.
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.
