Static timing analysis, properly
What the timing analyser computes, how setup and hold checks work with real numbers, how to read a Vivado or Quartus report, and what to fix first
You will learn
- Explain what a static timing analyser enumerates and why it never needs a simulation stimulus
- Compute setup and hold slack by hand for a register-to-register path with given delays, skew and uncertainty
- Locate arrival time, required time, logic levels and routing share in a Vivado or Quartus Prime timing report
- Distinguish WNS from TNS and use both to decide whether a design is close to closure or structurally too slow
- Choose the first fix for a failing path based on whether it is a constraint problem, a hold problem or a logic-depth problem
Every FPGA design that has to run at a stated clock frequency goes through static timing analysis (STA), and the report it produces is the single most important document the tools hand you. This lesson is for engineers who have built a design, seen a red “timing not met” line, and want to understand what the number means before touching the RTL. You will work through the setup and hold checks with real delays, learn where each figure sits in a Vivado or Quartus Prime report, and finish with a short decision procedure for what to fix first.
What the analyser actually computes
A simulator applies stimulus and watches values change. A timing analyser does neither. It reads the placed and routed netlist, the delay model for the chosen device and speed grade, and your constraints. Then it enumerates every path that starts at a clocked element (a flip-flop, a block RAM port, a DSP register, or an input pin) and ends at another clocked element or an output pin. For each path it adds up the worst-case delays through the logic and routing and compares that sum with the moment the capture register will sample.
Three things follow from this. First, the analysis is exhaustive: every path is checked, including ones your testbench never exercises. Second, it is pessimistic by design: it assumes the slowest possible combination of process, voltage and temperature for setup, and the fastest for hold. Third, it is only as good as the constraints. A path with no clock definition is not analysed at all, and a path with the wrong clock definition is analysed against the wrong deadline. The clock constraints themselves are covered in the cheat sheet at /reference/xdc-sdc-constraints/.
The diagram labels the five ingredients of every check: the clock path to the launch register, the launch register’s clock-to-output delay, the combinational logic, the routing, and the clock path to the capture register. If you need a reminder of why registers impose these deadlines at all, see /learn/beginner/04-registers-clocks-and-state/.
The setup check with real numbers
Take a 200 MHz clock, so the period is 5.000 ns. The path launches on one rising edge and must be captured on the next. Use these figures, which are typical of a mid-range 16 nm or 28 nm part in its slowest speed grade:
| Quantity | Symbol | Value |
|---|---|---|
| Source clock path (root to FF A) | 1.200 ns | |
| Clock-to-output of FF A | t_cko | 0.450 ns |
| Two LUT levels | logic | 0.600 ns |
| Routing between the LUTs and to FF B | route | 1.900 ns |
| Destination clock path (root to FF B) | 1.150 ns | |
| Clock uncertainty (jitter and phase error) | 0.035 ns | |
| Setup time of FF B | t_su | 0.080 ns |
The data arrival time is everything on the launch side added together:
arrival = 1.200 + 0.450 + 0.600 + 1.900 = 4.150 ns
The data required time is the capture edge, plus the delay for the clock to reach FF B, minus the margins that eat into it:
required = 5.000 + 1.150 - 0.035 - 0.080 = 6.035 ns
slack = required - arrival = 6.035 - 4.150 = +1.885 ns
Positive slack means the path passes with 1.885 ns to spare. Now suppose the router had to send that net across a congested region and the routing delay rose to 3.900 ns. Arrival becomes 6.150 ns and the slack is 6.035 minus 6.150, which is minus 0.115 ns. The path fails by 115 ps. That is a small number, and it is exactly the kind of failure that a better placement or a lower fanout fixes without any RTL change.
Note the role of clock skew. The capture clock arrived 0.050 ns earlier than the launch clock (1.150 ns against 1.200 ns), so the skew took 50 ps away from the budget. If the capture clock had been later than the launch clock the skew would have helped setup, and hurt hold. The general formulae are collected at /reference/timing-formulae/.
The hold check with real numbers
Hold is the check most people get wrong, because it has nothing to do with the clock period. It asks whether the new value launched on an edge can race through the logic and corrupt the capture register while that same edge is still being used to sample the old value. The check therefore uses the same clock edge at both ends, and it uses the fastest delays, not the slowest.
| Quantity | Fast value |
|---|---|
| Source clock path | 1.200 ns |
| Clock-to-output of FF A (minimum) | 0.250 ns |
| Logic (minimum) | 0.150 ns |
| Routing (minimum) | 0.200 ns |
| Destination clock path | 1.150 ns |
| Hold time of FF B | t_h |
arrival = 1.200 + 0.250 + 0.150 + 0.200 = 1.800 ns
required = 1.150 + 0.100 = 1.250 ns
slack = arrival - required = 1.800 - 1.250 = +0.550 ns
For hold the subtraction is reversed: data must arrive after the required time, so slack is arrival minus required. Here the data is held stable for 550 ps beyond what FF B needs. You can run this design at 1 MHz or 500 MHz and this number will not change.
Hold failures appear when the capture clock arrives much later than the launch clock. Common causes are a path between two clock trees that were never meant to be timed together, a clock passed through a LUT or a BUFG that another clock did not go through, or an input pin with no input delay constraint. The last of these is why a design with hold failures on I/O often has nothing wrong with the logic at all. Vendor routers insert delay to fix small internal hold violations automatically, which is why you rarely see them inside a well-constrained design.
One subtlety you will see in reports: the two clock paths share the segment from the root to the branch point, and that segment cannot be at its slowest and fastest delay at the same time. Both Vivado and the Quartus Prime Timing Analyzer add back the shared-segment difference under the name clock pessimism removal. It typically returns 20 ps to 100 ps and is not something you can influence.
Reading the report
Vivado and Quartus Prime lay the same information out differently, but once you know what to look for the mapping is direct.
| Concept | Vivado (report_timing) | Quartus Prime (Timing Analyzer) |
|---|---|---|
| Path endpoints | Source and Destination | From Node and To Node |
| Deadline | Requirement | Launch Clock and Latch Clock edges |
| Launch-side sum | Data Path Delay, then an Arrival Time column | Data Arrival Time |
| Capture-side sum | Required Time | Data Required Time |
| Logic vs routing | logic x ns (y%) and route z ns (w%) | Cell and interconnect columns per element |
| Depth | Logic Levels | Count the rows in the data arrival list |
| Shared clock credit | Clock Pessimism Removal | Clock Pessimism |
| Result | Slack (MET) or Slack (VIOLATED) | Slack, in the top line of the panel |
Three fields deserve a second look on every failing path.
Logic Levels (Vivado) or the number of cells in the arrival list (Quartus) tells you how deep the combinational cone is. At 200 MHz on a 28 nm part you can afford roughly six to eight LUT levels before routing alone exceeds the budget. On a modern 7 nm or 16 nm part the figure is higher, but the ratio of routing to logic is also higher, so depth still matters.
The logic to route percentage tells you whether the problem is your design or its placement. A path with 25 percent logic and 75 percent routing but only three LUT levels is a placement or fanout problem. A path with 60 percent logic across twelve levels is a pipelining problem.
The clock path skew line is the early warning for a constraint error. Internal skew within a single clock tree is usually under 150 ps. If you see a skew of several nanoseconds, the two ends are probably on different clocks or one clock is going through something it should not.
To get the summary and the worst paths from the Tcl console:
# Vivado, after route_design
report_timing_summary -max_paths 5 -file timing_summary.rpt
report_timing -sort_by group -max_paths 10 -nworst 1 -delay_type min_max
check_timing
# Quartus Prime Timing Analyzer (quartus_sta)
report_timing -setup -npaths 10 -detail full_path
report_timing -hold -npaths 10 -detail full_path
report_clocks
report_ucp
The last command in each block is the one people skip. Vivado’s check_timing and the Quartus “unconstrained paths” report list every endpoint the analyser ignored. A design that “meets timing” with 2,000 unconstrained endpoints has not met anything.
WNS, TNS and friends
The summary header of a Vivado report has six numbers: WNS, TNS, WHS, THS, WPWS and TPWS. Quartus reports slack per clock and a derived Fmax. The two that matter on a daily basis are the first pair.
Worst negative slack (WNS) is the slack of the single worst setup endpoint in the design. If it is positive, every setup check passes. If it is minus 0.115 ns, your worst path misses by 115 ps and everything else is better than that.
Total negative slack (TNS) is the sum of the negative slack of every failing endpoint. Each endpoint contributes at most its own worst path. If WNS is minus 0.115 ns and TNS is minus 0.230 ns, you have perhaps two failing endpoints and one targeted fix will probably close the design. If WNS is minus 0.115 ns and TNS is minus 48.000 ns, you have hundreds of endpoints each missing by a little. That pattern almost always means a global cause: a high-fanout net such as a reset or an enable, a congested region, or a clock period that is a few percent too aggressive for the device.
WHS and THS are the same figures for hold. WPWS and TPWS are pulse width checks, which fail when you ask a clock resource or a block RAM to run faster than its hard limit, and they are not fixed by anything in your RTL.
Quartus prints an Fmax per clock. Treat it as information, not a target. It is derived from the worst path at the current placement, and it also shows a “restricted Fmax” when a hard block limit, not your logic, is the ceiling.
What to fix first
Work through this order. Stopping at the first step that applies will save you from pipelining a design that only needed a constraint.
- Confirm the constraints describe the real board. Check the clock period against the oscillator or the PLL output. Check that every clock in report_clocks is one you intended, with the period you intended. Look at check_timing or the unconstrained path report. Add false paths only for paths that genuinely cross asynchronous boundaries, and use the patterns at /reference/reset-and-cdc-patterns/ rather than a blanket false path.
- Clear hold violations before setup violations. A hold failure breaks the design at every frequency, so it cannot be worked around by slowing the clock during bring-up. Almost every hold failure with a large magnitude is a clock path or constraint error, so this step is quick once step one is done.
- Read WNS and TNS together. A small WNS with a small TNS is a local problem: fix the worst path. A small WNS with a large TNS is a global problem: find the common cause with report_high_fanout_nets or the Quartus fanout report before touching any single path.
- Classify the worst path by its logic to route ratio and depth. Routing-heavy and shallow: reduce fanout with register duplication, floorplan the two blocks closer together, or remove the reset from datapath registers. Logic-heavy and deep: add a pipeline register, or restructure the arithmetic so the carry chain is shorter.
- Only then change the RTL. Pipelining changes latency, and latency changes the surrounding control logic. It is the right fix for a deep path, but it is the most expensive fix to verify.
A path you can run through the tools
The module below adds three 32-bit inputs in one cycle between registers. On most devices the two chained carry propagations plus routing land somewhere between 3 ns and 4 ns, which passes comfortably at 100 MHz and fails at 300 MHz.
module sum3 #(
parameter integer W = 32
) (
input logic clk,
input logic rst,
input logic [W-1:0] a,
input logic [W-1:0] b,
input logic [W-1:0] c,
output logic [W-1:0] y
);
logic [W-1:0] a_q, b_q, c_q;
always_ff @(posedge clk) begin
if (rst) begin
a_q <= '0;
b_q <= '0;
c_q <= '0;
y <= '0;
end else begin
a_q <= a;
b_q <= b;
c_q <= c;
y <= a_q + b_q + c_q;
end
end
endmodule
Constrain it for 300 MHz and report the worst path:
create_clock -period 3.333 -name clk [get_ports clk]
set_input_delay -clock clk 0.500 [get_ports {a[*] b[*] c[*]}]
set_output_delay -clock clk 0.500 [get_ports {y[*]}]
The report will show a path from one of the input registers through two carry chains to a bit of y. The Logic Levels count will be in the tens, because each carry element counts as a level, and the logic percentage will be high. That is the signature of a pipelining problem, which the second version fixes by splitting the addition across two cycles. The VHDL version is given because the structure is identical in either language and the analyser sees the same netlist from both.
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
entity sum3_pipe is
generic (
W : positive := 32
);
port (
clk : in std_logic;
rst : in std_logic;
a : in std_logic_vector(W-1 downto 0);
b : in std_logic_vector(W-1 downto 0);
c : in std_logic_vector(W-1 downto 0);
y : out std_logic_vector(W-1 downto 0)
);
end entity sum3_pipe;
architecture rtl of sum3_pipe is
signal a_q, b_q, c_q : unsigned(W-1 downto 0);
signal ab_q, c_d : unsigned(W-1 downto 0);
begin
process (clk)
begin
if rising_edge(clk) then
if rst = '1' then
a_q <= (others => '0');
b_q <= (others => '0');
c_q <= (others => '0');
ab_q <= (others => '0');
c_d <= (others => '0');
y <= (others => '0');
else
a_q <= unsigned(a);
b_q <= unsigned(b);
c_q <= unsigned(c);
ab_q <= a_q + b_q;
c_d <= c_q;
y <= std_logic_vector(ab_q + c_d);
end if;
end if;
end process;
end architecture rtl;
Rerun the report on the pipelined version and the worst path now holds one carry chain. The latency has gone from two cycles to three, which the surrounding logic must account for. A testbench that checks the sum at the right cycle is the way to be sure of that, using the techniques from /learn/beginner/06-simulation-and-testbenches/.
Exercise
Take the unpipelined sum3 module above and build it in Vivado or Quartus Prime for any board you own, with the 300 MHz constraint. Record WNS, TNS, the Logic Levels count and the logic percentage of the worst path. Then switch to sum3_pipe (write the Verilog yourself from the VHDL, or the reverse) and record the same four numbers. Finally, change the constraint to 100 MHz on the unpipelined version and compare the hold slack with the 300 MHz run. Write one sentence explaining why the hold slack did not change. The flow for building and reading reports is in /learn/beginner/10-from-rtl-to-board/.
Summary
- The analyser enumerates every clocked path and compares worst-case arrival against the capture clock, with no stimulus and no simulation.
- Setup slack is required time minus arrival time and depends on the period. Hold slack is arrival minus required and does not.
- In a report, Logic Levels and the logic to route percentage tell you whether to pipeline or to fix placement and fanout.
- WNS says how far the worst path misses. TNS says how many paths miss. A small WNS with a large TNS points to a global cause.
- Check constraints and unconstrained paths first, then hold, then setup, and change RTL only when the path is genuinely too deep.