Volume 03 Intermediate 5 sub-modules ~60 min read

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.

The report that should be empty 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 ...")
Split physical from timing, always Keep 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
Clock definition tree showing a primary clock on an input pin feeding an MMCM whose three outputs become automatically derived generated clocks ONE create_clock, FOUR CLOCKS ANALYSED sys_clk 100 MHz pin create_clock -period 10 MMCM M = 10, D = 1 VCO = 1000 MHz clk_out1 200 MHz (auto) clk_out2 100 MHz (auto) clk_out3 25 MHz (auto) derived for free Do NOT write create_generated_clock on an MMCM output. Two definitions on one pin means STA analyses the wrong one, silently.
Figure 3.2 - One 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]
A clock group is a promise, not a fix Declaring two domains asynchronous tells STA to stop checking those paths. It does not make the crossing safe. Every signal crossing that boundary still needs a proper synchroniser - 2-FF for a single bit, Gray-coded pointers or a handshake for a bus. The constraint and the synchroniser are two halves of one design decision; shipping the constraint alone is how metastability bugs reach the field. The full treatment is in Volume 05 of the Verilog course.

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.

Clock period budget divided between upstream clock-to-out, board trace delay, and the remaining time available inside the FPGA 10 ns BUDGET, THREE CLAIMANTS one clock period = 10.000 ns upstream Tco(max) 5.0 the other chip's datasheet +0.8 trace yours: 4.6 ns routing + logic + setup + uncertainty minus clock trace 0.4 input_delay(max) = Tco(max) + Tdata(max) - Tclk(min) = 5.0 + 0.8 - 0.4 = 5.4 ns input_delay(min) = Tco(min) + Tdata(min) - Tclk(max) = 1.5 + 0.5 - 0.6 = 1.4 ns max feeds the setup check; min feeds the hold check. You need both.
Figure 3.3 - Input delay is bookkeeping, not magic. Take the upstream device's clock-to-out from its datasheet, add the data trace delay, subtract the clock trace delay, and hand the result to STA.

# ==== 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[*]}]
A negative minimum output delay is normal -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:

"It fails timing, so I false-pathed it" is a bug report A false path does not make a path fast. It makes the tool stop looking. The signal still propagates, still takes 14 ns, and still gets captured mid-transition - you have simply removed the alarm. If you cannot articulate why the path can never be sampled at a moment that matters, it is not a false path.

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.

Waveform showing how a multicycle path moves the setup capture edge four cycles later and why the hold check must be moved back three cycles MULTICYCLE 4: SETUP MOVES FORWARD, HOLD MUST FOLLOW clk 0 1 2 3 4 launch default setup: 1 cycle -setup 4: capture at edge 4 4 x period available for logic hold now checked at edge 3 - impossible -hold 3 moves it back to edge 0
Figure 3.4 - Setting -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[*]]
The rule, in one line
Same-clock multicycle hold = setup − 1
For paths between two clocks of different frequency the arithmetic changes - the multiplier is applied in units of the capture clock for setup and the launch clock for hold - and you should verify with 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

Every pin in a bank shares one VCCO I/O pins are grouped into banks, and a bank has a single supply rail set by the board. You cannot put an 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_timing section for no_input_delay / no_output_delay. An unconstrained port is routed arbitrarily.
  • Are the numbers real? Re-derive Tco and 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.