Timing Closure
STA hands
you a report full of negative slack. Now what? Timing closure is the negotiation that
follows — between you, the tools, and physics — until every path's slack is non-negative at the target
clock. It is where chip design stops feeling like programming and starts feeling like diplomacy: you
state your terms, the tool counters with what silicon can actually do, and you both make concessions
until the report reads all zeros and pluses. Whole tape-out schedules live or die on how this
negotiation goes.
Constraints: the contract
The negotiation starts from a written contract — the constraints file (SDC). Its
clauses:
create_clock — the clock's period and source: the headline demand ("1.25 ns,
please").
- input/output delays — how much of the period is already spent outside this block,
so boundary paths are budgeted honestly.
- false paths — paths that exist in the gates but never in the function:
between two mutually-exclusive modes, from a configuration register written once at boot, across a
handshake that guarantees stability. Declaring one tells STA "do not check this"; the path vanishes
from the report and stops throttling optimisation.
- multicycle paths — data legitimately allowed N \ge 2
cycles to cross, because the receiving logic samples only every Nth
edge. The check relaxes to N \cdot T_{\text{clk}}.
Exceptions are power tools: each one deletes checks. Wielded correctly they recover real performance
that pessimism was wasting; wielded sloppily they hide real failures — see the Watch out below.
The fix toolbox, in escalating order
A path misses timing. The remedies come in a natural escalation — cheapest and most automatic first:
- Let the tools try harder. Upsize drive strength (swap a cell for its
beefier sibling — faster, at some area and power); buffer long wires; restructure
logic (rebalance a lopsided gate tree, factor differently, duplicate a gate to split its
load). Synthesis and the physical tools do all of this automatically when pushed.
- Pipeline the path. Add a register in the middle — the RTL move you already know
from the sequential-logic lessons: throughput is kept (one result per cycle), latency grows by one
cycle, and each half only has to fit half the logic in a period. This changes cycle-level behaviour,
so it is a designer decision, not a tool's.
- Retime. Slide existing registers through logic — moving a register from
a gate's output to all of its inputs (or vice versa) preserves the function while rebalancing the
stage delays. Tools do this legally and automatically when allowed; unlike pipelining it adds no
latency.
- Restructure the algorithm. When no amount of pushing gates helps, change the
maths: a ripple-carry adder's delay grows linearly in width, a carry-select or parallel-prefix
adder's logarithmically. The biggest timing wins in history are algorithm swaps, not gate
tweaks.
- Floorplan hints. Sometimes the delay is wire: two talkative blocks placed far
apart. Telling the back-end to keep them adjacent shortens the path without touching a single
gate.
Hold violations get the opposite medicine — adding small delay cells to too-fast paths — and
that fixing is usually left to the back-end flow, after placement, when the real wire delays are
known.
The flagship move, drawn
Here is pipelining as the timing tools see it — a chain of four logic blocks (0.9, 0.8, 1.1 and
0.7 ns) between two registers, with 0.15 ns of register overhead
(t_{cq} + t_{\text{setup}}) per stage:
Note where the cut went: after the second block, the most balanced split available. Cut after
the first block instead and the second stage would carry 2.6 ns — the clock is always set by the
worst stage, so an unbalanced cut wastes the register you spent. The little program below
tries every cut:
// Pipeline transform: where should the register go in a 4-block chain?
const blocks = [0.9, 0.8, 1.1, 0.7]; // ns of logic per block
const overhead = 0.15; // t_cq + t_setup per stage, ns
const sum = (a: number[]) => a.reduce((s, x) => s + x, 0);
const mhz = (t: number) => (1000 / t).toFixed(0) + " MHz";
const tBefore = sum(blocks) + overhead;
console.log("unpipelined: T = " + tBefore.toFixed(2) + " ns -> " + mhz(tBefore));
console.log("");
for (let cut = 1; cut < blocks.length; cut++) {
const s1 = sum(blocks.slice(0, cut)) + overhead;
const s2 = sum(blocks.slice(cut)) + overhead;
const T = s1 > s2 ? s1 : s2;
console.log("cut after block " + cut + ": stages " + s1.toFixed(2) + " / " + s2.toFixed(2) +
" ns -> T = " + T.toFixed(2) + " ns -> " + mhz(T));
}
console.log("");
console.log("The clock is set by the WORST stage - balance the cut.");
console.log("(Retiming is the tool doing exactly this search with your existing registers.)");
The closure loop — and its diminishing returns
Closure is iterative: synthesize → run STA → fix the worst paths → repeat. The first
loop kills the outliers cheaply (a few upsized cells); each following loop digs into a broader, harder
layer of paths. Progress follows a wall-of-slack curve: the worst negative slack improves quickly,
then crawls, because fixing one path promotes the runner-up to critical — whack-a-mole with
thousand-path moles. Experienced teams watch not just WNS (worst negative slack) but TNS
(total negative slack, the sum over all failing endpoints): WNS tells you how far physics is from the
contract, TNS tells you how much of the design is fighting you. When hundreds of endpoints
fail by a lot, no cell upsizing will save you — that is the signal to reach for the deeper tools:
pipeline, retime, or change the algorithm.
- constraints are the contract:
create_clock, input/output delays,
false paths (never-functional), multicycle paths (N cycles
allowed);
- fix order: tool moves (upsizing, buffering, restructuring) → pipeline
(throughput kept, latency +1) → retime (slide existing registers, no latency cost) → new
algorithm → floorplan hints;
- the loop: synthesize → STA → fix worst paths → repeat, with diminishing
returns; track WNS and TNS;
- hold fixing (inserting delay) is usually left to the back-end, once real wire
delays exist.
Because it is the moment the design becomes buildable. Before closure, the chip is a wish; a
single path 50 ps too slow means the advertised frequency is fiction. And closure is a genuinely
global fight: fixing one path can worsen another (that upsized cell just loaded its neighbour's
net), so late closure has the character of squeezing a balloon. On large industrial designs the last
few hundred picoseconds routinely consume weeks of engineer-and-compute time — farms of machines
rerunning synthesis and placement overnight, engineers triaging the morning's WNS like weather
forecasters. When the report finally shows no negative slack across all the analysis corners, the
design is declared closed — and somebody usually buys cake.
Declaring a false path does not make the path false — it makes the tool stop checking it.
The declaration is a promise, from you, that no real data ever needs to make that trip in one cycle;
STA takes your word and deletes the check, and nothing anywhere ever verifies the claim. Declare a
real path false and everything downstream looks perfect: synthesis stops optimising it, the
reports go green, the chip tapes out — and fails in the field, intermittently, at temperature, in the
hands of customers, with no warning and no log entry pointing home. This is why false-path sign-off
is a senior-engineer ritual at serious companies: each exception written down, argued for, and
reviewed by someone who did not write it. Treat every set_false_path you type as a small
signed affidavit.