FPGA

ILA (integrated logic analyzer) debug in FPGA

By CourseTron Team ·  7 min read ·  Updated Oct 2026 💬 𝕏 in f
Table of Contents
    ? Print / Save as PDF

    In short: An integrated logic analyzer (ILA) is a soft debug core synthesized into your FPGA fabric that captures selected internal signals into on-chip block RAM when a trigger condition fires, letting you see real waveforms of a running design without a single external probe.

    When a design behaves on the bench but passed every simulation, the problem usually lives in the gap between your testbench assumptions and real hardware: an asynchronous handshake, a clock-domain crossing, back-pressure from a DMA engine, or metastability on an input pin. Simulation cannot see those because they only exist once the bitstream is running at speed. The ILA closes that gap by turning the FPGA itself into an oscilloscope for its own nets.

    What an ILA actually is

    An ILA (Xilinx/AMD Vivado calls it ILA; Intel/Altera Quartus calls the equivalent Signal Tap; Microchip Libero has Identify/SmartDebug) is an RTL core that the tool instruments into your design. At its core it is three things: a bank of comparators that evaluate a trigger, a sample memory built from block RAM (BRAM), and a JTAG-connected control unit. You mark the nets you want to watch, the tool routes them to the core, and at runtime a host debugger streams the captured samples back over JTAG for display as a waveform.

    Because the ILA samples on your design clock, it sees exactly what the flip-flops see on each rising edge — a true functional view, not an analog guess. The practical ceiling is the fabric speed, often 300–600 MHz on modern devices.

    ILA core: data path User designprobe nets Triggercomparators Sample RAMBRAM capture JTAG ctrlto host PC CourseTron · coursetron.com
    Data path of an ILA core: probes feed both the trigger comparators and the capture RAM; JTAG ships results to the host.

    Probing internal signals

    There are two ways to attach probes. The simpler flow is "mark debug": you add (* mark_debug = "true" *) on a net in Verilog (or attribute mark_debug in VHDL), then run the tool's auto-insertion, which stitches an ILA onto every marked net post-synthesis. The second flow instantiates the core explicitly, giving you exact control over probe widths and count. Each probe is just a bundle of wires sampled synchronously, so you can watch a 32-bit AXI address bus on one probe and a single valid flag on another.

    The cost is real estate. Every probed bit consumes a fabric route and a slice of capture memory, and the trigger logic burns LUTs and flip-flops. Over-instrumenting a congested design can push timing closure out of reach, so probe the handful of signals that bound the bug — a state register, the handshake pair, and the data word — rather than an entire bus.

    Triggers: the heart of debug

    A trigger is a Boolean condition over the probed values that tells the core when to stop free-running and freeze the capture window. Basic match units compare a probe to a value with ==, !=, >, rising/falling edge, and don't-cares per bit. The power comes from combining them: a multi-stage "trigger state machine" can express "wait for fifo_full, then after 4 cycles catch the first wr_en && !rd_en." That is how you isolate a glitch that only appears on the third nested transaction.

    Trigger conceptWhat it doesTypical use
    Basic matchProbe equals/compares a constantCatch a specific opcode or address
    Trigger positionPre/post samples around the eventSee cause and effect in one window
    Trigger state machineSequenced, counted conditionsIsolate Nth occurrence of a pattern
    Capture (storage) conditionOnly store samples meeting a filterStretch effective depth on sparse events

    Trigger position is underused: set it to 50% and the window holds equal pre- and post-trigger history, so you see what led up to the fault as well as its aftermath — invaluable for a protocol violation that is a symptom, not the root cause.

    Capture depth and the BRAM budget

    The sample window is fixed by how much BRAM you dedicate. If you capture 64 bits of probe data at a depth of 1024 samples, that is 64 kbit, roughly two 36 kbit BRAMs on a 7-series device. Double the depth or the width and you double the memory. This is the central trade: wider visibility versus longer history versus leaving block RAM for the actual design.

    A "data depth" of 1024 at 200 MHz is only 5.12 µs of real time, which may be far too short for an event that happens once a millisecond. Two tactics stretch it: a capture/storage condition that only writes samples when something interesting is true (e.g. store only when axis_tvalid), and windowed capture that splits the RAM into N smaller buffers each armed by its own trigger. Both convert raw memory into targeted time coverage.

    Capture memory budget: 64 bit × 1024 64 bitprobe width × 1024samples deep = 64 kbittotal RAM ≈ 36 kbitBRAM 36 kbitBRAM 1024 samples × 5 ns (200 MHz) = 5.12 µs of history Double the width or the depth and the memory doubles. CourseTron · coursetron.com
    Capture memory scales with probe width times depth: a 64-bit by 1024-sample window is 64 kbit, about two 36 kbit BRAMs on a 7-series device.
    Trigger scanning the data stream clk data trig 0x00 → 0x01 → 0xFF (match) capture freeze CourseTron · coursetron.com
    The trigger pointer scans the stream each clock; when data == 0xFF the match fires and the core freezes the capture window.

    A practical debug session

    Suppose an AXI-Stream link occasionally drops a beat. Mark tvalid, tready, tlast, and the low byte of tdata with mark_debug. Set a depth of 2048 with trigger position at 25%, and a trigger-state-machine that waits for tvalid && !tready held for two cycles — the classic back-pressure stall. Rebuild, program the device, arm, and run your traffic. When the window freezes you will usually find the downstream consumer deasserting tready one cycle late, violating the handshake. That is a bug no static timing report will ever name for you.

    Remember the ILA changes the design: re-instrumenting means a fresh synthesis and a new bitstream and configuration download. Probe placement can also shift timing, so always re-check your closure after adding a core. For a deeper look at how the fabric routes these debug nets, see our note on clock-domain crossing, which is where a surprising share of real ILA findings originate.

    Want hands-on ILA labs on real Xilinx and Intel boards? Explore the FPGA debug track in our CourseTron course catalog and build the muscle memory that interviewers look for.

    Teams doing production bring-up often pair in-fabric debug with external instrumentation and design-for-debug reviews; the engineers at Avecas design services treat ILA strategy as part of RTL architecture, not an afterthought.

    Frequently asked questions

    Does an ILA affect my design's timing?

    Yes. The core adds routing, comparators, and BRAM, which increases congestion and can erode slack. Probe only essential signals and re-run timing analysis after inserting it.

    How deep a capture can I get?

    Depth is bounded by available block RAM. Total bits = probe width × sample depth; a 64-bit × 1024 capture is about 64 kbit. Use capture conditions or windowed triggers to extend effective time coverage without more RAM.

    Can I change triggers without rebuilding?

    Trigger values and comparisons are runtime-programmable over JTAG, so you can retarget conditions instantly. But adding new probed signals requires re-synthesis and a new bitstream.

    ILA versus an external logic analyzer?

    An ILA sees internal nets with zero extra pins and no probe loading, synchronous to your clock. An external analyzer sees only pinned-out signals but offers more depth and analog detail. They are complementary.

    Advertisement