The Vivado Flow: Synthesis to Bitstream
Most people press "Generate Bitstream" and wait. When it fails, they change something at random and press it again. The flow is actually nine well-defined commands, each with a precise contract about what it is allowed to change - and once you know those contracts, a timing failure stops being a mystery and becomes a question with an address.
2.1 Elaboration is not synthesis
Two entirely different transformations happen before you ever see a LUT, and conflating them is why "it elaborates fine but synthesis complains" feels arbitrary.
Elaboration resolves the language. Parameters are substituted,
generate blocks are unrolled, for loops are flattened, module
instances are bound, and the whole thing becomes a hierarchy of generic operators -
RTL_ADD, RTL_MUX, RTL_REG, RTL_ROM. Nothing device-specific has happened yet. You can
elaborate a design without even selecting a part.
Synthesis maps those generic operators onto the actual primitives in the device you chose. RTL_ADD becomes LUT2 plus CARRY4. RTL_REG becomes FDRE. RTL_ROM becomes either a RAMB36E1 or a pile of LUT6s depending on its size and shape. This step is where every decision from Volume 01 gets made on your behalf.
synth_design
is device-independent; everything right of it is not. Timing numbers before
route_design are estimates, and estimates lie.
What synthesis is allowed to do to your code
Synthesis is an optimiser, and optimisers delete things. Four transformations surprise people regularly:
| Transformation | What it does | When it bites |
|---|---|---|
| Constant propagation | A register that can only ever hold one value is replaced by that value | Your debug counter vanishes because nothing reads it |
| Equivalent register merging | Two flops with identical inputs become one flop | Your two-flop synchroniser collapses into one |
| Sequential pruning | Logic with no path to any output is deleted entirely | A whole module disappears; utilisation reads suspiciously low |
| Retiming | Registers move across combinational logic to balance path delay | Signal names in the timing report no longer match your RTL |
ASYNC_REG, and it is not optional. Full CDC theory lives in
Volume 05 of the Verilog course.
module sync_2ff (
input logic clk, // destination clock
input logic d_async, // from another domain - unrelated to clk
output logic q_sync
);
// ASYNC_REG does three things at once:
// 1. forbids merging these two flops into one
// 2. tells the placer to put them in the SAME slice (short route)
// 3. tells simulation not to propagate X on a timing violation
(* ASYNC_REG = "TRUE" *) logic meta_q;
(* ASYNC_REG = "TRUE" *) logic sync_q;
always_ff @(posedge clk) begin
meta_q <= d_async;
sync_q <= meta_q;
end
assign q_sync = sync_q;
endmodule
Interview grilling - "Your module synthesised to zero LUTs. What happened?"
Almost always one of three things, and you should name all three to show you have a process rather than a guess:
- No output path. Nothing downstream reads the module's outputs, or they are not connected to a top-level port. Sequential pruning removed everything. Check the synthesis log for "removing unused" messages - they are informational, not warnings, which is why nobody reads them.
- Constant inputs. A parameter or tie-off made the whole function constant. The classic version is a mode signal wired to a literal during bring-up and never wired back.
- Black box. The module was never actually elaborated - a missing file in the project, or a name mismatch - so Vivado inferred a black box and quietly carried on. This one prints a real warning, and it is the one to look for first.
The command to reach for is report_utilization -hierarchical, which shows
per-module resource usage. A module reporting zero rows is unambiguous.
2.2 Attributes and directives: steering the tool
You cannot rewrite Vivado's optimiser, but you can constrain it. Two mechanisms exist, and they operate at different scopes: attributes are written into the RTL and attach to a specific object; directives are command-line switches that change the whole run's strategy.
The attributes worth memorising
| Attribute | Effect | Survives to implementation? |
|---|---|---|
ASYNC_REG |
Protects and co-locates synchroniser flops | Yes - and it must |
DONT_TOUCH |
Forbids all optimisation of the object | Yes |
KEEP |
Preserves a net through synthesis only | No |
KEEP_HIERARCHY |
Stops cross-boundary optimisation for a module | Yes - useful for floorplanning and for readable reports |
MAX_FANOUT |
Replicates a driver when its fanout exceeds N | Yes |
RAM_STYLE |
block / distributed / ultra / registers |
Decided at synthesis |
SRL_STYLE |
register / srl / srl_reg / reg_srl_reg |
Decided at synthesis |
FSM_ENCODING |
one_hot / sequential / gray / johnson / none |
Decided at synthesis |
USE_DSP |
Forces or forbids DSP48 mapping | Decided at synthesis |
MARK_DEBUG |
Makes a net available to the ILA insertion flow | Yes - implies DONT_TOUCH on that net |
// ---- fan-out control -------------------------------------------------
// A reset or enable feeding 4000 flops has enormous fanout. Replicating
// the driver costs a handful of flops and can be worth 2 ns of slack.
(* MAX_FANOUT = 64 *) logic rst_n_int;
// ---- memory style ----------------------------------------------------
// 32 entries x 8 bits: too small to justify a whole BRAM tile.
(* RAM_STYLE = "distributed" *) logic [7:0] lut_table [0:31];
// ---- shift register style -------------------------------------------
// A 64-deep delay line maps to two SRL32s (2 LUTs!) instead of 64 flops.
// "srl_reg" adds one real flop at the output for better clock-to-out.
(* SRL_STYLE = "srl_reg" *) logic [63:0] delay_line;
// ---- FSM encoding ----------------------------------------------------
// Vivado chooses automatically and is usually right. Override only when
// you have measured a reason to.
(* FSM_ENCODING = "one_hot" *) state_e state, next_state;
// ---- debug -----------------------------------------------------------
// MARK_DEBUG keeps the net alive AND lets you attach an ILA probe to it
// from the netlist, without editing RTL again. See Volume 07.
(* MARK_DEBUG = "TRUE" *) logic [7:0] fsm_trace;
DONT_TOUCH on a top-level module because "the optimiser is breaking
something" disables constant propagation, register merging and retiming for everything
inside it. Designs have lost 30% of their fmax this way. Find the one
net or one register that actually needs protecting and mark only that. If you cannot
identify it, the bug is probably not the optimiser.
Directives: whole-run strategy
Each implementation command takes a -directive. These are not knobs you tune
randomly - each one trades runtime for a specific kind of quality, and there is a right order
to try them in when you are 200 ps short.
# Baseline: fast, and what the GUI runs by default.
synth_design -directive default
place_design -directive default
route_design -directive default
# Short on setup slack? Push the placer harder before touching RTL.
place_design -directive ExtraTimingOpt
phys_opt_design -directive AggressiveExplore
route_design -directive AggressiveExplore
# Running out of area, not time?
synth_design -directive AreaOptimized_high
# Routing congestion (many "overlap" messages, long route times)?
place_design -directive AltSpreadLogic_high
route_design -directive AlternateCLBRouting
# Reproducing a customer's exact result? Pin the seed.
place_design -directive default -seed 7
AggressiveExplore and fails at default, you
have roughly 100 ps of real margin and a design that will fail the next time anything
changes. Treat a passing sweep as permission to ship this build, and then go fix
the architecture. A healthy design closes on default with room to spare.
2.3 Opt, place, phys_opt, route
Implementation takes the synthesised netlist and turns it into physical reality. Four commands do the work, and each has a strict contract about what it may modify.
| Command | May change | May not change | Typical gain |
|---|---|---|---|
opt_design |
Netlist - prunes, remaps LUTs, propagates constants across the whole design | Nothing is placed yet | 5-15% area |
place_design |
Which physical site each cell occupies | The netlist itself | Sets the achievable floor for timing |
phys_opt_design |
Both - it can replicate a high-fanout driver, retime a register, or rewire a critical net, then re-place the affected cells | Design intent (it is timing-driven only) | 100-400 ps on the worst path |
route_design |
Which physical wires connect the placed cells | Placement, except for minor legalisation | Converts estimates into truth |
phys_opt_design is the one worth understanding properly, because it is the only
stage that can restructure logic with real placement information in hand. Before
placement, the tool guesses that a net will be short. After placement it knows the driver is
in clock region X0Y2 and one of its forty loads is in X3Y0. That knowledge unlocks fixes that
were invisible earlier:
- Fanout replication - duplicate a driver and give each copy the loads physically near it. This is the single highest-value physical optimisation.
- Critical-cell placement - pull the endpoints of the worst path closer together, accepting a small penalty everywhere else.
- Register retiming - push a flop forwards or backwards across combinational logic to rebalance two adjacent stages.
- DSP and BRAM register absorption - pull a fabric flop into the DSP's
PREGor the BRAM's output register, removing a routing hop entirely.
phys_opt_design -directive AggressiveExplore followed by
phys_opt_design -directive AlternateReplication.
2.4 Reading the reports
Two reports decide whether you ship. Everything else is supporting evidence.
report_utilization
+----------------------------+-------+-------+-----------+-------+
| Site Type | Used | Fixed | Available | Util% |
+----------------------------+-------+-------+-----------+-------+
| Slice LUTs | 28451 | 0 | 53200 | 53.48 |
| LUT as Logic | 25103 | 0 | 53200 | 47.19 |
| LUT as Memory | 3348 | 0 | 17400 | 19.24 |
| Slice Registers | 41120 | 0 | 106400 | 38.65 |
| Block RAM Tile | 98.5 | 0 | 140 | 70.36 |
| DSPs | 44 | 0 | 220 | 20.00 |
| Bonded IOB | 112 | 112 | 200 | 56.00 |
| BUFGCTRL | 5 | 0 | 32 | 15.63 |
+----------------------------+-------+-------+-----------+-------+
The rows that actually predict trouble are not the ones people look at:
- Block RAM at 70% is far more dangerous than LUTs at 53%. BRAM tiles are in fixed columns; once they are scarce, the placer must stretch logic across the die to reach the remaining ones, and routing congestion follows.
- "LUT as Memory" is your distributed RAM and SRLs. High numbers here mean SLICEM sites are being consumed, which reduces packing freedom (see §1.3).
- Half-tiles. "98.5" block RAMs means some 36Kb tiles are split into 18Kb halves. Fine - but it also means you cannot assume 140 - 98 = 42 usable tiles remain.
- Run it with
-hierarchicalwhen a number surprises you. Per-module attribution turns "where did 8000 LUTs come from" into a two-minute question.
Anatomy of a timing path
Everything in static timing analysis reduces to one comparison: does the data arrive before it is required? The theory is in Volume 07 of the Verilog course; here is what Vivado's version of it looks like.
| What the path looks like | Root cause | Fix |
|---|---|---|
| Logic delay dominant, many logic levels | Combinational depth | Pipeline it. Add a register stage in the middle of the cone. |
| Net delay dominant, few logic levels | Placement spread or high fanout | phys_opt_design, MAX_FANOUT, or a pblock |
| Net delay dominant, 2 logic levels, huge fanout number | One driver feeding thousands of loads (usually reset or enable) | Manual replication, or remove the reset entirely |
| Path crosses two different clocks | Missing CDC constraint | Do not fix the timing - fix the constraint. See Volume 03. |
| Enormous negative slack (> 5 ns) on one path | Almost always an unconstrained or wrongly constrained clock | Check report_clocks before touching RTL |
# The four commands that answer "why is my design failing timing", in order.
# 1. Is anything actually failing, and how badly?
report_timing_summary -delay_type min_max -max_paths 10 -file timing_sum.rpt
# 2. Show me the worst path with full detail.
report_timing -delay_type max -max_paths 1 -nworst 1 -significant_digits 3
# 3. Are my clocks what I think they are? (Catches the biggest failures.)
report_clocks
report_clock_interaction -delay_type min_max
# 4. Which nets have absurd fanout?
report_high_fanout_nets -timing -load_types -max_nets 20
Interview grilling - "WNS is -0.2 ns and TNS is -840 ns. What does that tell you?"
That the design has a systemic problem, not a critical path. With a WNS of only -0.2 ns, roughly four thousand endpoints must be failing by a small amount each to accumulate -840 ns of total negative slack.
One bad path and thousands of marginal paths need opposite responses:
- WNS -3 ns, TNS -3 ns - one path. Find it, pipeline it, done. An afternoon's work.
- WNS -0.2 ns, TNS -840 ns - the whole design is marginal. The clock is probably 10-15% too fast for this architecture, or the device is too full and everything is being routed the long way round. Directive sweeps will not save this; either the frequency target or the architecture has to move.
The follow-up worth volunteering: "I would also check report_design_analysis
-congestion, because a TNS that large with a small WNS is the classic signature of
routing congestion rather than logic depth."
2.5 Bitstream, configuration and the Tcl flow
write_bitstream serialises the routed design into the configuration bits that
will be shifted into the device's SRAM at power-up. Two things about it matter in practice.
First, it runs design rule checks that nothing earlier runs. An unconnected
I/O buffer, a missing CFGBVS setting, or a pin assigned to a bank whose voltage
contradicts its I/O standard will all pass place-and-route and fail here - after the twenty
minutes you just spent routing. Run report_drc after
opt_design to catch these early.
Second, an FPGA's configuration is volatile. The bitstream lives in external flash and is loaded on every power-up:
| Mode | Source | Use |
|---|---|---|
| JTAG | Cable, direct from Vivado | Development. Volatile - gone at power-off. |
| Master SPI / QSPI | On-board flash, FPGA clocks itself | Production. Requires an .mcs written with write_cfgmem. |
| Slave SelectMAP | An external CPU pushes bytes in | Systems where a host already exists |
| Zynq PS boot | The ARM cores load the PL from the boot image | Every Zynq design - see Volume 06 |
Non-project mode: the flow you can put in git
A Vivado .xpr project is a directory of tool state that changes every time you
open it, which makes it close to useless in version control. Non-project mode is a Tcl script
that reads your sources, runs the flow and writes outputs - reproducible, diffable, and the
way essentially every production team runs builds.
# ---------------------------------------------------------------------
# Non-project build. Run with: vivado -mode batch -source build.tcl
# ---------------------------------------------------------------------
set part xc7a100tcsg324-1
set top system_top
set outdir ./build
file mkdir $outdir
# ---- read sources (order does not matter; Vivado resolves it) --------
read_verilog -sv [glob ./rtl/*.sv]
read_xdc ./constraints/timing.xdc
read_xdc ./constraints/pins.xdc
# ---- synthesis -------------------------------------------------------
synth_design -top $top -part $part -directive default
write_checkpoint -force $outdir/post_synth.dcp
report_utilization -file $outdir/post_synth_util.rpt
# ---- implementation --------------------------------------------------
opt_design
report_drc -file $outdir/drc.rpt ;# catch pin/bank errors EARLY
place_design
phys_opt_design -directive AggressiveExplore
write_checkpoint -force $outdir/post_place.dcp
route_design
write_checkpoint -force $outdir/post_route.dcp
# ---- sign-off --------------------------------------------------------
report_timing_summary -file $outdir/timing.rpt
report_utilization -file $outdir/util.rpt
# Fail the build if timing did not close. Without this, CI will happily
# publish a bitstream that does not work.
set wns [get_property SLACK [get_timing_paths -delay_type max]]
if {$wns < 0} {
puts "ERROR: setup timing failed, WNS = $wns ns"
exit 1
}
write_bitstream -force $outdir/$top.bit
exit 0
SLACK and exiting non-zero is what turns a build script into a gate.
Without it every commit produces a bitstream, including the ones that fail timing by two
nanoseconds - and someone will program it, and it will half-work, and the bug report will
say "intermittent". Check WNS and WHS (hold), and fail on either.
Interview grilling - "Why did the same RTL give a different result on the second run?"
Placement and routing are heuristic searches over an enormous space. They are deterministic given identical inputs, but "identical" is stricter than people assume:
- Tool version. Different Vivado versions have different cost functions. This is the most common cause of "it built last month".
- Seed.
place_design -seed Nchanges the starting point. Different seed, different local optimum, up to several hundred picoseconds apart on a marginal design. - Thread count. Multi-threaded routing can produce slightly different
results depending on core count. Pin it with
set_param general.maxThreads 4if bit-exact reproducibility matters. - Incremental compile. If a previous checkpoint is being reused, the result depends on that checkpoint, not only on the RTL.
The real point to make: a design whose timing outcome depends on the seed has no margin. Reproducibility problems are usually margin problems wearing a disguise.
Volume 02 recap
| Concept | The one thing to remember |
|---|---|
| Elaboration | Resolves the language. Device-independent, no LUTs yet. |
| Register merging | Will eat your synchroniser. ASYNC_REG is mandatory. |
DONT_TOUCH |
Scope it to one net. On a module it costs real performance. |
phys_opt_design |
The only stage that restructures logic knowing real placement. |
| Logic vs net delay | Net-dominant means placement. Logic-dominant means pipeline. |
| WNS vs TNS | One bad path, or a thousand marginal ones. Different fixes. |
| Tcl flow | Fail the build on negative slack, or CI is decoration. |