Electronics Design AU
FPGASolved

Vivado ILA never fires — probes show constant 0 in Hardware Manager after adding mark_debug

7 min read3 replies
Original Question

Asked by stale_biscuit_03 ·

Trying to add ILA debug probes to an FPGA design I've been working on — Basys3 board (Artix-7), Vivado 2023.1, design is a hand-written SPI controller in Verilog. The SPI actually works fine, I can see the clock and MOSI on a scope and the peripheral responds. Now I want to observe the internal state machine signals to understand the timing.

Here's what I did:

  • Added (* mark_debug = "true" *) to three signals in the SPI module: spi_clk_en, tx_busy, and state (3-bit FSM state register)
  • Synthesised — Vivado printed "Found 3 signals for debug" in the log, so it seemed to recognise them
  • Opened synthesised design, ran Tools → Set Up Debug, accepted defaults, set the ILA clock to clk_50 (50 MHz from the onboard PLL)
  • Re-ran implementation, generated bitstream, programmed the board
  • Hardware Manager shows the ILA with three probes — probe names look right

Problem: I click "Run Trigger (Single)" and the status bar goes to "Armed" and just stays there. I've run a bunch of SPI transfers while it's armed and nothing captures. Tried changing the trigger to tx_busy == 1'b1 and same result — armed, waiting, nothing.

Is there something I missed in the wizard, or is the trigger setup different to what I'm expecting?

From the knowledge baseFPGA Development Flow: From HDL to Working Hardware

3 Replies

fpga_philosopher
Accepted Answer

There's a useful two-step split here. Before touching trigger settings at all, verify the probes are actually connected to your signals — because if they're not, no trigger configuration will ever work.

Run Trigger Immediately first

In Hardware Manager, right-click the ILA → "Run Trigger Immediately" (or the lightning bolt button). This bypasses trigger conditions and captures whatever is on the probe buses right now.

Look at the captured waveforms. If tx_busy, spi_clk_en, and state all show constant 0 — even while you're running SPI transfers — the probes are not connected to live signals. The ILA core exists in the bitstream but the synthesis tool removed the nets it was supposed to observe.

If free-run capture shows the signals moving, skip to the trigger section.


Root cause: mark_debug without keep

This is where the HDL attribute model trips people up. (* mark_debug = "true" *) tells the synthesis tool to expose the net to the debug infrastructure. It does not instruct the tool to preserve the net as a physical wire.

Synthesis is free to absorb a net that has no downstream fanout beyond the ILA probe — absorbing it into an adjacent LUT, renaming it during hierarchy flattening, or removing it if it considers the signal to be a redundant copy of something else. The ILA probe port then ends up tied to a constant, and Hardware Manager dutifully shows you that constant.

The fix is to add keep alongside mark_debug:

(* mark_debug = "true", keep = "true" *) reg [2:0] state;
(* mark_debug = "true", keep = "true" *) wire spi_clk_en;
(* mark_debug = "true", keep = "true" *) reg tx_busy;

keep is a synthesis directive that preserves the net as a named, observable point regardless of optimisation. Alternatively, add both properties in your XDC:

set_property MARK_DEBUG true [get_nets {spi_clk_en}]
set_property KEEP        true [get_nets {spi_clk_en}]

After adding keep, re-run synthesis, re-run Set Up Debug, re-run implementation, generate a new bitstream, and re-program. Then test free-run again.

Verify using the Tcl console after synthesis

Before going all the way to bitstream, open the synthesised design and run:

get_nets -hierarchical -filter {MARK_DEBUG == 1}

If your three nets are missing from the output, the attributes didn't attach — either because they were optimised away before the property could be preserved, or because the XDC net name doesn't match the post-synthesis flattened name (hierarchy flattening renames nets — spi_ctrl/spi_clk_en may become spi_clk_en_0 or similar in the flat netlist). If you see the nets listed, the marks are in place and the issue is purely the missing keep.


If free-run works but the trigger never fires

Once you've confirmed the probes are live, the trigger configuration is the next thing to look at.

The default trigger operator is == (EQUAL), which fires when the probe equals a constant value on every sampled clock edge. This works well for a signal that holds for many cycles. But tx_busy sounds like it's a pulse — high for the duration of a transfer, then back to 0. At 50 MHz, a 16-bit SPI transfer (assuming one bit per clock) lasts about 320 ns — 16 clock cycles. Whether the ILA catches that depends on how long the trigger evaluation window is and where you're sampling.

Change the trigger operator to (R) — rising edge — so the ILA fires on the 0→1 transition of tx_busy rather than checking for a static 1:

  • Click the operator dropdown next to tx_busy in the trigger setup panel
  • Select (R) (rising edge transition, 0 → 1)

Also check the trigger position. The default is 512 out of 1024 samples (50%), meaning the trigger event lands in the middle of the capture buffer — you see half the buffer before the trigger and half after. Move it to around 10% so you capture the full SPI transfer after the rising edge of tx_busy.

One more check: make sure the ILA clock (clk_50 in your case) matches the clock domain your SPI signals are in. If the state register is in a different clock domain than the ILA clock port, the probes will sample at the wrong rate and may show aliased or constant values. The complete synthesis → implementation → programming pipeline is covered in the FPGA development flow guide — ILA insertion sits between synthesis and implementation, so changes to probe assignments always require re-running both.

glitch_getter

The free-run test is the right first move — I run it on every new ILA bring-up before setting any trigger conditions. Eliminates an entire category of issue immediately.

A couple of things worth checking once free-run is working and you're trying to get the trigger to fire reliably:

Look at the free-run waveform and verify that tx_busy actually reaches the value you're triggering on during an SPI transfer. I've seen designs where tx_busy is active-low — it sits at 1 normally and pulses to 0 during the transfer. If that's the case, triggering on tx_busy == 1 will never fire because it's already 1 when idle. The scope won't tell you the polarity of internal signals; the free-run ILA capture will.

For the state probe: if you're triggering on a specific FSM state, double- check the bit ordering. Vivado displays probe bus bits MSB-first in the waveform viewer, but the trigger comparison value is also entered MSB-first. That part is consistent — but if you derived the expected state value from a binary literal in your source (3'b101 = 5) and you're entering a decimal 5 in the trigger field, that's fine. Where it breaks down is if the FSM state encoding in synthesis differs from what you expect — one-hot encoding vs binary, for example. After a free-run capture, read the actual value of state during the phase you want to trigger on, then use that value directly in the trigger.

One more practical thing: once armed, the ILA stays armed waiting for the trigger. If your SPI master runs a transfer and the trigger still doesn't fire, click "Run Trigger Immediately" while armed to force a capture at that instant. This lets you see what the signals looked like at the moment you expected the trigger to fire — often shows you the value mismatch immediately.

timing_budget_tamara

Adding one more failure mode for completeness, though it probably doesn't apply here.

If your SPI controller were instantiated inside a block synthesised out-of-context (OOC) — the default mode for IP Catalog blocks in Vivado — then get_nets in the top-level XDC won't reach signals inside that block. OOC synthesis runs the IP as an isolated compilation; its internal nets aren't visible from the top-level constraint context.

For a hand-written Verilog module in a standard project flow (not Block Design), this doesn't apply — everything synthesises in-context by default. But if you later move the SPI controller into an IP or bring it in via IP Integrator, keep this in mind. The fix is either to add the MARK_DEBUG and KEEP constraints to the block's own synthesis XDC, or set the block to global synthesis mode (right-click → Properties → Synthesis Options → Out-of-context synthesis: Disable).

For the current problem, keep on the source signals and the rising-edge trigger change are almost certainly the full fix. The Tcl verification step (get_nets -hierarchical -filter {MARK_DEBUG == 1}) after synthesis will confirm it quickly.

Related Discussions