Timing Constraints (SDC)
The SDC timing contract: clocks (create_clock, generated, virtual), input/output delay and the I/O budget, uncertainty and latency (pre/post-CTS), exceptions, clock groups and case analysis.
Synthesis has no idea how fast your chip must run until you tell it. The SDC (Synopsys Design Constraints) file is where you state the timing requirements: how fast the clock is, when data arrives at the inputs and is required at the outputs, and which paths are special. Every timing number the tool reports is computed against this file, so constraints that are wrong or missing produce a netlist that is optimised for the wrong thing. SDC is the single most important input to both synthesis and STA.
What SDC constrains
An SDC answers four questions for the timing engine: what are the clocks and how fast do they run; when does data arrive at, and when must it be valid at, the chip's ports; how much margin to hold back for real-world clock imperfection; and which paths should be timed differently or not at all. Get these right and the tool optimises the paths that matter; get them wrong and it wastes effort, or, worse, leaves real paths unchecked.
Defining the clock, create_clock
Everything starts with the clock. create_clock names a clock, sets its period (the frequency target) and its waveform (duty cycle / edges), and attaches it to a source, usually an input port or a pin. A 500 MHz target is simply create_clock -period 2.0 -name clk [get_ports clk]. Until a clock exists, no register-to-register path has a requirement, so nothing is really being timed.
Generated and virtual clocks
Not every clock comes from a port. A create_generated_clock is derived on-chip from a master clock, a divide-by-2, a clock-gate output, a multiplexed clock, and the tool needs to know its relationship to the master so it times the crossing correctly. A virtual clock has no physical source at all; it exists only to be the reference for I/O delays when the real clock lives outside the block. Both are common and both are easy to forget, which silently mis-times whole regions.
I/O timing, input and output delay
Your block does not live alone: some other logic drives its inputs and some other logic consumes its outputs. You model that external world with set_input_delay and set_output_delay, both stated relative to a clock. Input delay says how much of the period is already used up by upstream logic before data reaches your input port; output delay says how much the downstream logic needs before its own capture edge. What is left in the middle is the time your combinational logic actually has.
Each uses -max for the setup (long-path) check and -min for the hold (short-path) check. A missing I/O constraint is dangerous precisely because it fails quietly: an unconstrained input or output path is simply not checked, so it looks clean in the report while being wide open in silicon.
Model the driver and the load too, set_driving_cell gives input ports a realistic driver (so input transition is real, not ideal) and set_load puts realistic capacitance on output ports. Without them the tool assumes perfect edges and under-buffers the boundary.
Clock uncertainty and latency
Real clocks are not perfect, so you subtract a margin. set_clock_uncertainty holds back time for skew and jitter. Before CTS you set it generously (it stands in for the skew you have not built yet plus jitter); after CTS you switch to set_propagated_clock so the tool uses the real, built clock latencies and only jitter plus a small margin remain. set_clock_latency separately models source latency (before the clock root, e.g. the PLL and top route) and network latency (the on-chip tree).
Pre-CTS vs post-CTS, before the clock tree exists, use an ideal clock with a pessimistic set_clock_uncertainty to cover the unknown skew. Once CTS is done, set_propagated_clock makes timing use the actual insertion delays, and you drop the skew portion of uncertainty. Forgetting to propagate leaves you signing off on a fake, zero-latency clock.
Timing exceptions
Not every path that exists is a path that matters, and telling the tool so stops it chasing timing that is not real. The three exceptions you will use constantly:
- set_false_path, a path that never carries real timing-critical data (e.g. a static config register, or a true asynchronous crossing). The tool stops timing it entirely.
- set_multicycle_path, a path that is allowed more than one clock cycle to settle. You tell the tool how many cycles, and it relaxes the requirement accordingly.
- set_max_delay / set_min_delay, a direct point-to-point delay override, used when neither a false nor a multicycle path fits (often on asynchronous or custom interfaces).
Clock groups
When a design has several clocks that are unrelated, say a 500 MHz core clock and a 100 MHz peripheral clock with no fixed phase relationship, timing paths between them are meaningless and should not be analysed. set_clock_groups -asynchronous tells the tool these clocks never time against each other (those crossings are handled by CDC synchronizers instead), and -physically_exclusive / -logically_exclusive handle clocks that can never be active at the same time, like a functional clock and a test clock on the same pin.
Case analysis
set_case_analysis pins a constant onto a signal, typically a mode or test-enable pin, so the tool analyses only the mode you actually run in. Setting a scan-enable to 0, for example, tells STA to time the functional mode and ignore the scan paths that are irrelevant in normal operation.
SDC command reference
| Command | Models | Example |
|---|---|---|
| create_clock | A clock: period, waveform, source | create_clock -period 2.0 [get_ports clk] |
| create_generated_clock | A derived/divided/gated clock | create_generated_clock -divide_by 2 ... |
| set_input_delay | Upstream arrival at an input port | set_input_delay -max 0.8 -clock clk [ports] |
| set_output_delay | Downstream requirement at an output | set_output_delay -max 0.7 -clock clk [ports] |
| set_clock_uncertainty | Skew + jitter margin | set_clock_uncertainty -setup 0.1 [clk] |
| set_clock_latency | Source / network insertion delay | set_clock_latency -source 0.3 [clk] |
| set_propagated_clock | Use real (post-CTS) clock latencies | set_propagated_clock [all_clocks] |
| set_false_path | Exclude a path from timing | set_false_path -from [cfg_reg] |
| set_multicycle_path | Allow more than one cycle | set_multicycle_path 2 -setup -to [ports] |
| set_max_delay / set_min_delay | Point-to-point delay override | set_max_delay 1.5 -from A -to B |
| set_clock_groups | Unrelated / exclusive clocks | set_clock_groups -asynchronous -group {..} |
| set_driving_cell / set_load | Realistic input driver / output load | set_load 0.05 [get_ports data_out] |
| set_case_analysis | A constant on a mode/test pin | set_case_analysis 0 [get_ports scan_en] |
Garbage constraints, garbage timing. An over-tight clock makes the tool over-build the whole design for slack you will never use; a missing I/O or exception constraint leaves real paths unchecked so the report looks green while silicon fails. Constrain honestly and completely, the SDC is the one file every downstream tool trusts absolutely.
Recap: SDC is the timing contract: define clocks (create_clock, generated, virtual), model the outside world with input/output delay and driving-cell/load, reserve margin with uncertainty and propagate the clock after CTS, and clean up the analysis with exceptions, clock groups and case analysis. Every timing number depends on it.