FPGA timing analysis and constraint management for EconoPET gateware. Use for any task involving SDC files, timing constraints, timing violations, setup or hold failures, clock definitions, I/O delay constraints, false paths, multicycle paths, timing reports, timing closure, or static timing analysis (STA).
Investigate and resolve timing failures for the EconoPET FPGA gateware using the Efinix Efinity static timing analysis (STA) tools.
cmake --build --preset gw)
so that timing data exists for analysis.| File | Purpose |
|---|---|
| ./reference/design.md |
| Hardware design specification (clocks, components, timing parameters). Read this first. |
| gw/EconoPET/EconoPET.sdc | Project SDC file (clock definitions, I/O delays, false paths) |
| build/gw/outflow/EconoPET.pt.sdc | Auto-generated constraint template listing every port and clock that the Interface Designer expects to be constrained |
| build/gw/outflow/EconoPET.pt_timing.rpt | Timing report from the most recent gateware build (GPIO pad delays, PLL info) |
The STA shell is an interactive TCL REPL. Do not launch the STA shell as a background process. Always pipe commands non-interactively:
echo '<tcl commands>; exit' \
| .github/skills/timing-analysis/scripts/sta-repl.sh 2>&1
Before reading files, running commands, or editing constraints, create or
update .cache/ai/timing-analysis.md:
touch .cache/ai/timing-analysis.md
Keep a running log for the entire task. After every step, append:
Every constraint recommendation, SDC edit, and timing-closure conclusion must be evidence based. If the log does not contain evidence that justifies a decision, do not make that decision yet. Gather more evidence first and record it in the log.
Read these files in parallel before doing anything else. All are required to make correct constraint decisions:
Record in .cache/ai/timing-analysis.md what each file says that is relevant to
the current issue. Cite exact paths, line numbers, clocks, ports, and timing
assumptions.
Run check_timing first. It is the single most informative command, reporting
exactly how many ports are unconstrained:
echo 'check_timing; exit' \
| .github/skills/timing-analysis/scripts/sta-repl.sh 2>&1
Add -verbose to list every violating pin. The output table shows:
| Section | What it means |
|---|---|
no_clock | Register pins without a clock constraint (should be 0) |
unconstrained_internal_endpoints | Internal paths with no timing check (should be 0) |
no_input_delay | Input ports missing set_input_delay or set_false_path |
no_output_delay | Output ports missing set_output_delay or set_false_path |
multiple_clock | Pins driven by multiple clocks (CDC concern) |
Target: 0 unconstrained inputs and 0 unconstrained outputs. Ports with
set_false_path count as "constrained" (reported separately in the output).
Then run timing summary and the worst setup/hold paths:
echo 'report_timing_summary; exit' \
| .github/skills/timing-analysis/scripts/sta-repl.sh 2>&1
echo 'report_timing -from_clock sys_clock_i -to_clock sys_clock_i -setup; exit' \
| .github/skills/timing-analysis/scripts/sta-repl.sh 2>&1
echo 'report_timing -from_clock sys_clock_i -to_clock sys_clock_i -hold; exit' \
| .github/skills/timing-analysis/scripts/sta-repl.sh 2>&1
If you need more than one path, use -npaths and optionally -nworst:
echo 'report_timing -from_clock sys_clock_i -to_clock sys_clock_i -setup -npaths 10 -nworst 1; exit' \
| .github/skills/timing-analysis/scripts/sta-repl.sh 2>&1
To check CDC paths between clock domains:
echo 'report_cdc -details; exit' \
| .github/skills/timing-analysis/scripts/sta-repl.sh 2>&1
To list all post-synthesis ports (ports optimized away by synthesis are removed):
echo 'puts "INPUTS:"; puts [all_inputs]; puts "OUTPUTS:"; puts [all_outputs]; exit' \
| .github/skills/timing-analysis/scripts/sta-repl.sh 2>&1
Log each command, the important output, and what it proves. Note: Efinity does
not support -max_paths. Use -npaths / -nworst instead.
Every port in the post-synthesis netlist needs a constraint. Read the RTL
(gw/EconoPET/src/top.sv, main.sv, and relevant submodules) to classify each
port. This step is critical -- the constraint type depends on the signal driver.
Record the RTL evidence for each classification decision in the log.
| Category | Constraint | How to identify |
|---|---|---|
| Interface-synchronized output | Copy set_output_delay from <project>.pt.sdc | Template already includes interface and clock-network timing, often with -reference_pin |
Registered (always_ff) | set_output_delay relative to the real or virtual interface clock | Driven by always_ff @(posedge <clock>) |
| Combinational from a clocked domain | set_output_delay relative to the real or virtual interface clock | assign or always_comb fed by registers in one domain |
| True multicycle synchronous path | set_multicycle_path | Same logical clock relationship, but capture intentionally occurs N cycles later |
| Multi-cycle external protocol with no meaningful STA launch/capture clock | set_false_path only with RTL/protocol proof | Example: externally observed OE or bus phase signal that is not sampled on the same STA clock edge |
| Constant or tied off | set_false_path | assign port = 0 or assign port = 1 |
| Different clock domain | set_output_delay -clock <other_clock> | Driven by registers in another clock domain |
| Debug / unused | set_false_path | Not connected to functional external devices |
| Category | Constraint | How to identify |
|---|---|---|
| Interface-synchronized input | Copy set_input_delay from <project>.pt.sdc | Template already includes interface and clock-network timing, often with -reference_pin |
| Source-synchronous (e.g., SPI SDI) | set_input_delay -clock <source_clock> | Sampled on external clock edge |
| True multicycle synchronous path | set_multicycle_path | Same logical clock relationship, capture intentionally occurs N cycles later |
| Bus-protocol synchronized with no meaningful STA clock relation | set_false_path only with RTL/protocol proof | Sampled during FPGA-controlled slots where ordinary edge-based STA does not model the protocol |
| Async / quasi-static | set_false_path | DIP switches, open-drain signals, async external signals |
| CDC with synchronizer | set_false_path from the port | Crossed via 2-FF synchronizer into sys_clock_i domain |
| Unused / optimized away | No constraint needed | Removed by synthesis (not in all_inputs output) |
Efinix FPGAs split tristate I/O into _o (data), _oe (output enable), and
_i (input) ports. OE signals frequently have long combinational paths through
address decoding gated by cpu_be_o. Because the bus arbiter toggles BE
multiple sys_clock_i cycles before data transitions, OE changes have a
multi-cycle budget. Use set_false_path -to [get_ports {*_oe[*]}] for these.
Trap: Constraining OE signals with set_output_delay causes setup
violations on paths like cpu_be_o -> address_decode -> cpu_addr_oe[*] because
the combinational depth exceeds one sys_clock_i period. This does not indicate
a real timing problem.
Do not generalize this to all synchronous outputs. Prefer set_multicycle_path
for genuinely synchronous multi-cycle behavior. Use set_false_path only when
the path should not be analyzed at all.
Read build/gw/outflow/EconoPET.pt.sdc.
It is regenerated each time the gateware is compiled and contains a commented-out
set_input_delay or set_output_delay stub for every port/clock pair that the
Interface Designer expects to be constrained. It also contains create_clock
statements for PLL and pad-entry clocks.
Use it as a checklist:
create_clock in the template has a corresponding definition
(or an intentional omission with a comment) in the project SDC.set_input_delay,
set_output_delay, or set_false_path in the project SDC. Unconstrained ports
are left with default timing and may cause silent failures.Edit gw/EconoPET/EconoPET.sdc. After editing, you can re-read the SDC to check for syntax errors and re-analyze timing against the existing netlist:
echo 'reset_timing; delete_timing_results; read_sdc; report_timing_summary; exit' \
| .github/skills/timing-analysis/scripts/sta-repl.sh 2>&1
This validates the constraints parse correctly and gives a first look at timing impact, but the netlist has not been re-optimized for the new constraints. Setup violations against the old netlist may resolve after a full rebuild (step 6).
Before each SDC edit, write the proposed change in the log, cite the supporting
evidence, and explain why the chosen constraint is the correct model. If you
reject an alternative (set_false_path, set_multicycle_path, different clock,
etc.), record why.
If a setup violation appears on an output path that was previously unconstrained,
check whether the path has a multi-cycle budget. If so, use set_false_path
instead of set_output_delay.
Constraint changes that affect which paths are optimized (adding/removing output delays, false paths, clock definitions) require a full rebuild so that synthesis and place-and-route can optimize for the new constraints:
cmake --build --preset gw
After the rebuild completes, re-run the STA shell to confirm timing is clean:
echo 'report_timing_summary; exit' \
| .github/skills/timing-analysis/scripts/sta-repl.sh 2>&1
Then run check_timing again to verify 0 unconstrained ports remain. Append the
final verification results, residual risks, and the final decision summary to
.cache/ai/timing-analysis.md.
Order constraints exactly as follows (dependencies require this sequence):
create_clock)create_clock without a target)create_generated_clock)set_clock_groups)set_input_delay, set_output_delay)set_false_path)
b. Max/min delays (set_max_delay, set_min_delay)
c. Multicycle paths (set_multicycle_path)Always define a clock before referencing it in any I/O delay or exception constraint. A constraint referencing an undefined clock is silently ignored.
When the same path has multiple exceptions, Efinity applies them in this order (highest priority first):
set_clock_groupsset_false_pathset_max_delay / set_min_delayset_multicycle_path# starts a comment.\ at end of line continues the command on the next line.* is a wildcard (e.g., Oled* matches Oled0, Oled_en).get_ports, get_pins, get_cells, get_nets object specifiers.
The pipe | separates instance name from port name (e.g., FF1|Q).-regexp option on object specifiers. Escape
brackets when using regex.Use a virtual clock as the set_input_delay/set_output_delay reference when
the board clock is off-chip. This removes interface clock latency and uncertainty
from I/O constraints. Put the virtual clock and core clock in the same clock
group so the timer analyzes transfers between them.
Use a real pad clock when the relationship is to the FPGA pad clock. Use
-reference_pin for synchronized or forwarded-clock interfaces.
Default rule:
set_input_delay / set_output_delay constraints from
<project>.pt.sdc. Keep -reference_pin when present.<project>.pt_timing.rpt.Synchronized I/O (copy from <project>.pt.sdc):
| Direction | Max | Min |
|---|---|---|
| Input | DDATA_max + tCO + DCLK_INTERFACE_max | DDATA_min + tCO + DCLK_INTERFACE_min |
| Output | DDATA_max + tSETUP - DCLK_INTERFACE_max | DDATA_min - tHOLD - DCLK_INTERFACE_min |
Negative minimum output delay is valid. Do not clamp to zero.
Unsynchronized I/O (bypass mode, receive clock):
| Direction | Max | Min |
|---|---|---|
| Input | board_max + GPIO_IN_max | board_min + GPIO_IN_min |
| Output | board_max + GPIO_OUT_max | board_min + GPIO_OUT_min |
For forward-clock I/O, add -reference_pin and include GPIO_CLK_OUT. See
the Efinity Timing Closure User Guide for the four forward-clock modes.
GPIO_CLK_IN delay is in set_clock_latency, not in I/O delay constraints.
Some CPU-bus-facing signals use false paths or simplified delays because the bus
arbiter in timing.sv creates a protocol-level multi-cycle budget. Justify from
./reference/design.md and RTL, not as a general rule.
Match the template form with explicit -name and [get_ports]:
create_clock -period $period -name sys_clock_i [get_ports {sys_clock_i}]
Common mistake: using -name without a target creates a virtual clock. That is
correct only when you intentionally want an off-chip reference clock. Efinity
prints an info message when it finds a virtual clock definition.
Use -add to define multiple clocks on the same target (e.g., dynamic clock
muxes). Without -add, a later create_clock on the same target overwrites
the previous one.
# Explicit all-groups form (preferred for this project)
set_clock_groups -asynchronous -group {sys_clock_i} -group {spi0_sck_i}
# Single-group form: cuts this group to all other clocks
set_clock_groups -asynchronous -group {spi0_sck_i}
Default (no flag) is -exclusive. Use -asynchronous for independent clock
sources. Use -exclusive for clocks that never operate simultaneously (e.g.,
dynamic mux). Prefer the all-groups form: it documents known relationships and
requires explicit updates when adding clocks.
Default single-cycle: setup = 1, hold = 0. To shift by N cycles with width M:
setup = N, hold = M - 1.
For different clock frequencies, use -start/-end to select the reference:
-start for setup (hold defaults to -start)-end for hold (setup defaults to -end)Do not use both -start and -end on the same constraint.
| Cause | Fix |
|---|---|
| Long combinational output path (e.g., OE through address decode) | Use set_false_path only if the path should not be timed. Otherwise use set_multicycle_path or change RTL |
| Genuinely tight internal path | Add pipeline register in RTL |
| Overly tight output delay | Recalculate from interface timing, external setup/hold, board delay, and pt_timing.rpt |
| Path between unrelated clocks | set_clock_groups or set_false_path |
| Cause | Fix |
|---|---|
| Short registered path (e.g., FF to BRAM port) | P&R placement issue, not SDC -- try seed sweep or pipeline register |
| Negative min output delay | Recalculate it. Negative min can be correct for synchronized outputs |
| Missing clock group declaration | Add set_clock_groups -asynchronous |
Internal register-to-register hold failures are P&R placement issues, not SDC
problems. Options: seed sweep, set_min_delay (sparingly), or pipeline register.
Warning: set_min_delay and set_max_delay override clock-derived timing.
They can mask real violations -- the software honors your input without error,
but the issue appears on the board. Avoid unless no alternative exists.
| Mistake | Symptom | Fix |
|---|---|---|
Latch inferred from incomplete if/case | Combinational loop warning, timing graph cut at arbitrary point | Complete all assignments, use default in case |
Unintended virtual clock (-name without target) | Info message about virtual clock, I/O paths not analyzed | Add [get_ports {...}] target |
| Undefined clock | Unconstrained registers, unoptimized logic | Define all clocks, check check_timing for no_clock entries |
| Wrong constraint order | Silently ignored constraints, unexpected timing | Follow the ordering in Constraint Order |
When constraints are correct but timing still fails, try these approaches in