← All articles

Closing timing on an Artix-7: what the IronKernel 64 build taught us

Clock domains, crossings, I/O constraints and build gates from an open-source Artix-7 design that closes timing on every release build.

IronKernel ·

Artix-7 timing closure gets easier when the constraints describe what the design does and the build refuses to produce a bitstream that misses timing. This article walks through the Vivado timing closure work on IronKernel 64, our open-source SID music machine on a Digilent Nexys Video with an AMD (formerly Xilinx) Artix-7 XC7A200T. The release build closes with a worst negative slack (WNS) of 0.083 ns, a worst hold slack (WHS) of 0.012 ns and 0 failing endpoints in Vivado 2026.1. The constraints quoted here are in the public repository.

The clock domains on the Artix-7

One 100 MHz oscillator feeds the system MMCM. Two more MMCMs make the video and audio clocks, and the DDR3 controller and the Ethernet PHY add their own.

ClockFrequencyUse
cpu_clk100 MHzBoth MicroBlaze V cores and the register blocks
sid_clk50 MHzThe SID core and network protocol logic
clk200200 MHzDDR3 controller IDELAY reference
ui_clk100 MHzDDR3 controller user side
clk125, clk125_90125 MHzRGMII transmit and its 90-degree forwarded clock
axis_clk62.5 MHzNetwork client side
pix_clk, pix_clk_x574.25 / 371.25 MHz720p60 video and the TMDS serializers
aud_clk12.288 MHzAudio codec, 256 × 48 kHz
eth_rxck125 / 25 / 2.5 MHzRGMII receive, from the PHY

The design uses synchronous resets, one reset synchronizer per clock domain, no derived clocks and no clock gating.

One primary clock and no blanket clock groups

The board XDC declares one primary clock, the 100 MHz oscillator. Vivado derives the MMCM outputs and the DDR3 controller clocks from it. Only the two forwarded I/O clocks, for RGMII transmit and the QSPI flash, get a hand-written generated clock.

The XDC declares no set_clock_groups. Declaring two clocks asynchronous with set_clock_groups stops Vivado from timing any path between them, including a crossing added later without a synchronizer. IronKernel 64 constrains each crossing on its own instead.

Clock domain crossings: synchronizers first, then scoped constraints

Every crossing in the top level goes through one of a few blocks, and a Tcl script gives each class the constraint its protocol needs after synthesis:

  • Single-bit levels use a 3-stage synchronizer chain marked ASYNC_REG, with a false path only to the first flip-flop.
  • Gray-coded counters get a datapath-only max delay and a bus skew limit of one source clock period.
  • The AXI4-Lite register crossing from cpu_clk to sid_clk holds its data steady while a toggle crosses, and the data gets a max delay of one destination period.
  • The audio and SID stream FIFOs are AMD xpm_fifo_async macros, and the DDR3 path uses the AMD AXI clock converter IP. Both carry their own constraints.

The script fails the build when an expected crossing is missing or the number of crossings changes, so a renamed register cannot lose its constraint without a message. The build also runs report_cdc and fails on any Warning or Critical finding that has no written waiver.

False paths only where no clock relationship exists

The board-level false paths cover pins with no timing relationship to an internal clock: the reset button, UART, PS/2, HDMI hot-plug, the I2C bus, LEDs, the OLED and slow Ethernet PHY side-band signals. The audio I2S outputs are also false paths, because they leave from IOB flip-flops and the codec bit clock runs at only 3.072 MHz.

When the RTL does not close: change the clock

The SID core is the open-source reDIP-SID. At 100 MHz its unmodified filter multiply-add misses timing with a WNS of -0.187 ns. The multiply-add is combinational and the filter logic relies on that zero latency, so the design keeps it unchanged. The core needs at least 20 clocks per SID clock phase, so the design runs it on a 50 MHz sid_clk, which gives 25 or 26 clocks per phase.

Pipelining is the usual fix for a long combinational path. When the logic around it depends on the latency, a slower clock for that block can be the cleaner fix.

I/O timing: RGMII and QSPI

On RGMII transmit, the forwarded clock first had its generated clock rooted on the ODDR output, which left the output buffer out of the clock path. The result was a false setup violation of -1.46 ns. Rooting the generated clock on the output pad times clock and data through the same path, and the release build shows positive setup and hold margins on transmit and receive.

The QSPI configuration flash runs through the AXI Quad SPI IP and the STARTUPE2 primitive. With its output-enable registers left in the fabric, the build missed timing on those paths by 2.19 ns. Placing them in the IOBs and following the constraint recipe in AMD's product guide for the IP closed the interface.

The implementation flow and its gates

The build is a non-project Vivado Tcl script. After synthesis it runs opt_design, place_design, phys_opt_design and route_design with the Explore and AggressiveExplore directives, and runs phys_opt_design again if routed WNS is negative.

Each failure class has its own exit code, and nothing is written after a failed gate:

  • Setup, hold or pulse-width slack below 0, or any failing endpoint.
  • Unclocked or unconstrained endpoints, or loops, from check_timing.
  • LUT, flip-flop, block RAM or DSP use above 80 %.
  • Latches, or asynchronous resets outside a named list.
  • Critical warnings in implementation, or methodology findings without a written waiver.

Block RAM sits at 78.49 % of the device, so the small OLED framebuffer uses distributed RAM on purpose.

A Vivado timing closure checklist

The IronKernel 64 build follows these steps:

  1. Run check_timing and fix unconstrained endpoints before reading slack.
  2. Read report_timing_summary for setup, hold and pulse-width slack.
  3. Read report_clock_interaction to see which clocks exchange data.
  4. Put a synchronizer or an asynchronous FIFO on every crossing, and constrain each one by cell instead of grouping whole clocks.
  5. Run report_cdc and report_methodology, and fail the build on their findings.
  6. Pipeline long paths where latency allows, or move the block to a slower clock.
  7. Use placement and routing directives and phys_opt_design once the constraints are right.
  8. Check I/O timing against the data sheet of the device on the other side of the pins.

Our FPGA design service covers this work on AMD devices, from the first constraint file to a gated build.

Further reading

Need help closing timing on an AMD FPGA?

We do RTL, clock-domain crossings, constraints and timing closure for Artix-7 and other AMD devices.