Constraints: XDC, Clocks & I/O Planning
Static timing analysis does not check your design. It checks the constraints you wrote about your design. A path you never described is not a fast path - it is an unanalysed one, and it will be routed by whatever is convenient. Most "it works in simulation but not on hardware" stories end here, in a file nobody reviewed.
report_timing_summary has a section called "check_timing"
listing every unconstrained endpoint, missing input delay and undefined clock. Most
engineers scroll past it to look at WNS. That is backwards: a green WNS on a design with
400 unconstrained endpoints means the tool analysed the easy 60% of your design and
ignored the rest.
3.1 XDC fundamentals: it is a Tcl program
An .xdc file is not a declarative list. It is a Tcl script that Vivado executes
top to bottom against the in-memory design database. Three consequences follow immediately,
and each of them causes real bugs.
1. Order matters
Constraints are applied in the order they are read. A set_false_path that
references a clock defined later in the file matches nothing - silently. Vivado will not
error, because at the moment the line ran, get_clocks sys_clk legitimately
returned an empty list. The canonical ordering is:
# --- 1. Primary clocks ------------------------------------------------
# Nothing else can be written until the clocks exist.
create_clock -period 10.000 -name sys_clk [get_ports sys_clk_p]
# --- 2. Generated / derived clocks ------------------------------------
# MMCM and PLL outputs are automatic. Only hand-written dividers,
# forwarded clocks and gated clocks need an explicit statement.
# --- 3. Clock relationships -------------------------------------------
set_clock_groups -asynchronous \
-group [get_clocks sys_clk] \
-group [get_clocks eth_rx_clk]
# --- 4. I/O delays ----------------------------------------------------
set_input_delay -clock sys_clk -max 4.2 [get_ports {data_in[*]}]
set_input_delay -clock sys_clk -min 1.1 [get_ports {data_in[*]}]
# --- 5. Timing exceptions (last - they refine everything above) ------
set_false_path -from [get_cells cfg_reg_reg[*]]
set_multicycle_path 4 -setup -from [get_cells slow_acc_reg[*]]
set_multicycle_path 3 -hold -from [get_cells slow_acc_reg[*]]
2. Precedence is not the same as order
When two constraints could apply to the same path, the more specific one wins regardless of file order. The priority ladder, highest first:
| Priority | Constraint | Note |
|---|---|---|
| 1 (highest) | set_false_path |
Removes the path from analysis completely |
| 2 | set_max_delay / set_min_delay |
Absolute override, ignores clock periods |
| 3 | set_multicycle_path |
Relaxes the edge, keeps the clock relationship |
| 4 (lowest) | Clock period from create_clock |
The default requirement for everything else |
Within one priority level, more specific object sets win:
-from A -to B beats -from A, which beats a bare clock-to-clock
statement.
3. Empty object lists are silent
This is the number one source of constraints that "do not work". If
get_cells u_fifo/wr_ptr_reg[*] matches nothing because you renamed the instance,
the constraint applies to nothing and the flow continues. There is exactly one habit that
prevents it:
# WRONG - if the pattern misses, this line does nothing, forever.
set_false_path -from [get_cells u_cfg/mode_reg[*]]
# RIGHT - fail loudly at constraint-read time.
set cfg_cells [get_cells -quiet u_cfg/mode_reg[*]]
if {[llength $cfg_cells] == 0} {
error "XDC: u_cfg/mode_reg[*] matched no cells - check hierarchy names"
}
set_false_path -from $cfg_cells
# Vivado also flags misses on its own if you ask it to:
# set_msg_config -id {Vivado 12-180} -new_severity ERROR
# (12-180 is "No valid object(s) found for ...")
pins.xdc (package pins, I/O standards, drive strength) separate from
timing.xdc (clocks, delays, exceptions). Physical constraints are
board-specific and change when you respin the PCB; timing constraints are design-specific
and change when you change the architecture. Mixing them means every board revision forces
a review of your clock definitions.
3.2 create_clock and generated clocks
A clock definition tells STA three things: the period, the waveform (duty cycle and phase), and the point in the netlist where it starts. Everything downstream is derived.
# A 100 MHz single-ended clock arriving on a pin.
create_clock -period 10.000 -name sys_clk [get_ports sys_clk]
# The same, but declaring a 60/40 duty cycle: rise at 0, fall at 6 ns.
create_clock -period 10.000 -waveform {0.000 6.000} -name skewed_clk \
[get_ports skewed_clk]
# A 125 MHz clock arriving on a differential pair - constrain the P side
# only. The IBUFDS is a single cell; constraining both pins creates two
# clocks on one buffer.
create_clock -period 8.000 -name gt_refclk [get_ports gt_refclk_p]
# A VIRTUAL clock: no netlist object at all. Used as the reference for
# I/O delays when the external device is clocked by something the FPGA
# never sees.
create_clock -period 10.000 -name virt_sys_clk
create_clock on the input pin gives you the
whole tree. Vivado reads the MMCM's multiply and divide settings from the instantiated
primitive and derives every output clock automatically.
When you do write a generated clock
Three cases, and only three: a clock divided in fabric, a clock forwarded off-chip through an
ODDR, and a gated clock through a BUFGCE.
# 1. Fabric divider. (Volume 01 argued against these - but if a legacy
# block has one, it must be constrained or STA sees no clock at all.)
create_generated_clock -name clk_div2 \
-source [get_pins clk_div_reg/C] -divide_by 2 [get_pins clk_div_reg/Q]
# 2. Forwarded clock leaving the chip through an ODDR. The generated
# clock is what the DOWNSTREAM device sees, so output delay must
# reference this, not sys_clk.
create_generated_clock -name spi_sclk_out \
-source [get_pins oddr_sclk/C] -divide_by 1 [get_ports spi_sclk]
# 3. Gated clock through BUFGCE. Same period, but a distinct clock
# object so you can constrain the enable path separately.
create_generated_clock -name clk_gated \
-source [get_pins bufgce_i/I] -divide_by 1 [get_pins bufgce_i/O]
Declaring domains asynchronous
Two clocks with no fixed phase relationship - a 100 MHz system clock and a recovered 125 MHz Ethernet receive clock, say - must be declared unrelated. Otherwise STA invents a common period and tries to close paths between them that no amount of routing can fix.
# One direction only. Paths from
# eth_rx_clk INTO sys_clk are still
# analysed, still fail, and still
# waste hours of router effort.
set_false_path \
-from [get_clocks sys_clk] \
-to [get_clocks eth_rx_clk]
# Both directions, one statement,
# and it also covers every future
# path between the two domains.
set_clock_groups -asynchronous \
-group [get_clocks sys_clk] \
-group [get_clocks eth_rx_clk]
3.3 Input and output delay budgeting
Inside the FPGA, Vivado knows every delay exactly. Outside it, Vivado knows nothing - not the
trace lengths, not the other chip's clock-to-out, not the connector. set_input_delay
and set_output_delay are how you hand over that missing information, and they
are the constraints most often either omitted or copied from a template without the numbers
being changed.
The mental model: your clock period is a budget shared between three parties - the upstream device, the board, and you. Input delay declares how much of the period is already spent before the signal reaches your pin.
# ==== INPUT: an ADC driving 12 bits into the FPGA ====================
# ADC Tco : 5.0 ns max, 1.5 ns min
# Data trace: 0.8 ns max, 0.5 ns min
# Clock trace: 0.6 ns max, 0.4 ns min
set_input_delay -clock sys_clk -max 5.4 [get_ports {adc_d[*]}]
set_input_delay -clock sys_clk -min 1.4 [get_ports {adc_d[*]}]
# ==== OUTPUT: the FPGA driving a DAC ================================
# DAC setup : 2.0 ns, DAC hold : 0.5 ns
# Data trace: 0.9 ns max, 0.6 ns min
# Clock trace: 0.7 ns max, 0.5 ns min
#
# output_delay(max) = Tsu + Tdata(max) - Tclk(min) = 2.0 + 0.9 - 0.5 = 2.4
# output_delay(min) = -Th + Tdata(min) - Tclk(max) = -0.5 + 0.6 - 0.7 = -0.6
set_output_delay -clock sys_clk -max 2.4 [get_ports {dac_d[*]}]
set_output_delay -clock sys_clk -min -0.6 [get_ports {dac_d[*]}]
# ==== Genuinely asynchronous pins ===================================
# A push-button, an LED, a DIP switch. These have no timing relationship
# to any clock. Constraining them with a made-up number is worse than
# excluding them - but they must be EXCLUDED EXPLICITLY, so that
# check_timing does not list them as unconstrained.
set_false_path -from [get_ports {btn[*] dip[*]}]
set_false_path -to [get_ports {led[*]}]
-min -0.6 looks like a typo and is not. It encodes "the downstream device's
hold requirement is small enough, and the board skew favourable enough, that data may
legally arrive slightly before the clock edge". Copying a template that uses
-min 0 silently tightens your hold requirement by 600 ps and can turn a
working interface into an unroutable one.
3.4 False paths and multicycle paths
Exceptions tell STA that the default assumption - launch on one edge, capture on the next - is wrong for a particular path. Used correctly they recover enormous amounts of slack. Used carelessly they hide real failures.
False paths: "never check this"
Legitimate uses are narrow. A path is genuinely false only when the data on it can never be captured in a way that matters:
- Static configuration. A register written once at boot and read forever after. If the value changes only while the consumer is held in reset, no edge relationship exists to check.
- Synchroniser inputs. The first flop of a 2-FF synchroniser is
expected to go metastable; checking its setup time is meaningless. Usually
covered by
set_clock_groupsrather than a per-path exception. - Test and debug logic that is never active in a timing-critical mode.
- Truly asynchronous I/O - buttons, LEDs, status pins.
Multicycle paths: "check it later"
A multicycle path is the honest version. The data genuinely takes several cycles, the consumer genuinely waits, and you are telling STA the truth about the real relationship. It needs two statements, and the second one is the one everybody forgets.
-setup 4 alone leaves the hold check
demanding that data stay stable for three whole cycles after launch. The
-hold 3 statement puts the hold check back where physics expects it.
# The router now has to satisfy an
# absurd 3-cycle hold requirement.
# Symptoms: hours of route time,
# hundreds of inserted LUT delays,
# and WHS still negative.
set_multicycle_path 4 -setup \
-from [get_cells acc_reg[*]] \
-to [get_cells result_reg[*]]
# The pair. For same-clock paths the
# hold value is ALWAYS setup - 1.
set_multicycle_path 4 -setup \
-from [get_cells acc_reg[*]] \
-to [get_cells result_reg[*]]
set_multicycle_path 3 -hold \
-from [get_cells acc_reg[*]] \
-to [get_cells result_reg[*]]
report_timing -from ... -to ... that the edges landed where you
intended. Never assume; read the report.
Interview grilling - "When is a multicycle path safe?"
Only when the hardware itself guarantees the receiver ignores the data for those extra cycles. Two structures make that guarantee:
- An enable that is only asserted every N cycles. The destination register's CE is driven by a counter or FSM that provably cannot fire in between.
- A handshake. The consumer asserts ready only after the producer has signalled valid, and the valid path itself is single-cycle constrained.
What is not a guarantee: "the software only writes that register once a second", or "in practice it never changes that fast". Those are statements about typical behaviour, and STA is about worst case.
A good closing point: "I would also add an assertion in simulation that the destination enable never asserts within N cycles of the source changing. That turns the constraint's assumption into something the testbench actually checks."
3.5 Pins, I/O standards and drive
Physical constraints look trivial - a pin name and a voltage standard - and they are where bitstream generation most often fails after a successful route.
# ---- basic single-ended pin -------------------------------------------
set_property PACKAGE_PIN E3 [get_ports sys_clk]
set_property IOSTANDARD LVCMOS33 [get_ports sys_clk]
# ---- an output that drives a long trace -------------------------------
# DRIVE is in mA; SLEW trades edge rate against ground bounce and EMI.
set_property PACKAGE_PIN H5 [get_ports {led[0]}]
set_property IOSTANDARD LVCMOS33 [get_ports {led[0]}]
set_property DRIVE 12 [get_ports {led[0]}]
set_property SLEW SLOW [get_ports {led[0]}]
# ---- a button that needs a pull-up ------------------------------------
set_property PACKAGE_PIN D9 [get_ports btn_n]
set_property IOSTANDARD LVCMOS33 [get_ports btn_n]
set_property PULLUP true [get_ports btn_n]
# ---- a differential input pair ----------------------------------------
# Constrain the P side; Vivado derives N from the package. DIFF_TERM
# enables the on-die 100 ohm termination - omit it and you need a
# resistor on the board instead.
set_property PACKAGE_PIN U4 [get_ports gt_refclk_p]
set_property IOSTANDARD LVDS_25 [get_ports gt_refclk_p]
set_property DIFF_TERM true [get_ports gt_refclk_p]
# ---- board-level configuration ----------------------------------------
# Omitting CFGBVS/CONFIG_VOLTAGE is the classic "DRC failed after a
# 40-minute route" error on 7-series parts.
set_property CFGBVS VCCO [current_design]
set_property CONFIG_VOLTAGE 3.3 [current_design]
# Tie unused pins low rather than leaving them floating inputs.
set_property BITSTREAM.CONFIG.UNUSEDPIN PULLDOWN [current_design]
The bank rule that catches everyone
LVCMOS33 output and an LVCMOS18 output in the same
bank, no matter what the XDC says - Vivado will reject the combination at DRC. And on
high-performance (HP) banks in UltraScale devices, 3.3 V is not
supported at all; the maximum is 1.8 V. Choosing pins before checking the bank map is
how a board respin happens.
| Property | What it controls | Getting it wrong |
|---|---|---|
IOSTANDARD |
Voltage levels, termination, single vs differential | DRC error, or a signal the other chip cannot read |
DRIVE |
Output current, hence edge rate into a load | Too low: slow edges. Too high: ringing and ground bounce |
SLEW |
SLOW / FAST edge shaping |
FAST everywhere is a reliable way to fail EMC testing |
DIFF_TERM |
On-die 100 Ω across an LVDS pair | Double termination if the board also has a resistor |
PULLUP / PULLDOWN |
Weak internal bias (tens of kΩ) | Not a substitute for a real pull-up on an I2C bus |
CFGBVS |
Configuration bank voltage select | DRC failure at write_bitstream, after routing |
Interview grilling - "Timing closed, but the interface is corrupt on hardware. Where do you look?"
Timing closing proves the constraints were satisfied. It says nothing about whether the constraints described reality. Work outward:
- Was the interface constrained at all? Check the
check_timingsection forno_input_delay/no_output_delay. An unconstrained port is routed arbitrarily. - Are the numbers real? Re-derive
Tcoand the trace delays from the datasheet and the PCB stack-up. Template numbers copied from a different board are worse than none, because they look deliberate. - Right clock reference? A source-synchronous interface must reference the forwarded clock, not the internal system clock. This is the single most common structural error in I/O constraints.
- Signal integrity. If the constraints check out, the problem has left the digital domain: termination, reflections, ground bounce from too many simultaneously switching outputs on one bank.
The honest sentence that impresses: "Closing timing means my design is consistent with my constraints. Verifying the constraints against the datasheet is a separate review step, and it is the one I would do first."
Volume 03 recap
| Concept | The one thing to remember |
|---|---|
| XDC is Tcl | Order matters, and an empty get_* fails silently. |
| check_timing | Read it before WNS. Unconstrained beats "met". |
| MMCM outputs | Derived automatically. Never define them yourself. |
| Async domains | set_clock_groups, not two false paths. |
| I/O delay | Datasheet Tco + data trace - clock trace. Both max and min. |
| Multicycle | Two statements. Hold = setup - 1. |
| I/O banks | One VCCO per bank. HP banks cannot do 3.3 V. |