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:

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:

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.

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.