DEV Community

Hedy
Hedy

Posted on

How to debug FPGA Verilog HDL code?

Here’s a practical, repeatable workflow to debug Verilog on FPGAs—fast feedback in sim, then confirm on hardware.

1) Start with simulation (catch 90% here)

  • Unit testbenches: one per module; randomize inputs and include edge cases.
  • Waveforms: dump VCD/FST and inspect (GTKWave/Questa/ModelSim/etc.).
  • Golden model: compare RTL against a simple behavioral/C++/Python/Matlab reference.
  • $display/$monitor: log key events; gate verbose prints with ifdef TB.
  • Assertions (SystemVerilog works with Verilog RTL):
// handshake: ready cannot drop during an accepted transfer
property p_ready_stable;
  @(posedge clk) (valid && ready) |=> ready;
endproperty
assert property(p_ready_stable);

// FSM one-hot
assert property (@(posedge clk) $onehot0(state));
Enter fullscreen mode Exit fullscreen mode

Coverage: ensure every state/branch toggles (line/branch/toggle/functional).

Testbench skeleton

`timescale 1ns/1ps
module tb_top;
  reg clk=0, rst=1;
  always #5 clk = ~clk;           // 100 MHz
  initial begin
    repeat(5) @(posedge clk); rst=0;
  end

  // DUT
  dut u_dut(.clk(clk), .rst(rst), /* ... */);

  // stimulus + checks
  initial begin
    // directed tests
    // random tests
    // self-checks and $finish
  end
endmodule
Enter fullscreen mode Exit fullscreen mode

2) Avoid classic sim–synth mismatches

  • Blocking vs nonblocking: use nonblocking (<=) in clocked always_ff and blocking (=) in pure combinational always_comb.
  • Latch inference: give default assignments in combinational blocks.
  • Reset strategy: prefer synchronous resets; ensure all regs get a known value. Don’t rely on uninitialized power-up.
  • Delays/#: don’t use in synthesizable RTL.
  • Width/sign: match widths; watch signed math, shifts, and comparisons.
  • Case statements: use unique case (SV) or include full default.
  • CDC (clock domain crossing): 2-FF sync for single bits; async FIFOs or handshakes for buses; add CDC constraints.
  • X-propagation: in sim, drive unknowns on illegal paths to catch bugs early.

3) Timing & constraints (many “bugs” are timing)

  • Write clean XDC/SDC constraints (clocks, generated clocks, I/O delays).
  • Close setup/hold at the target frequency before blaming RTL.
  • Review false/multicycle paths; only declare them if truly justified.
  • For tough paths: pipeline, retime, or reduce fanout. Re-run STA.

4) Gate-level / timing simulation (when needed)

After synth or place&route, run gate-level sim with SDF on critical blocks (resets, CDC bridges, SERDES). It’s slower, but catches race/timing issues the RTL sim can’t.

5) Hardware debug (after sim is clean)

On-chip logic analyzers:

  • Xilinx: ILA/VIO; Intel: SignalTap; Lattice: Reveal.
  • Mark signals: (* mark_debug = "true" *) (Xilinx) or “preserve/keep” attributes so they survive optimization.
  • Trigger on protocol events (e.g., valid && ready && error).

Simple hooks:

  • LEDs/GPIO for state/FSM breadcrumbs.
  • UART “printf” from RTL via a tiny TX (great for counters/errors).
  • Spare pins to export clocks/handshakes to a scope/LA.

Backpressure tests on real I/O: throttle AXI-Stream/valid-ready to shake out stalls.

6) Common motor-sized checklist (copy/paste for any project)

When something “doesn’t work”:

  1. Clock present/locked? Reset released after PLL locks?
  2. Inputs stable and in the right domain? (Use synchronizers.)
  3. FSM has a valid reset state and progresses (add a timeout assert).
  4. FIFOs never overflow/underflow (add counters + assertions).
  5. External interfaces meet I/O timing (apply board delays in constraints).
  6. Resource conflicts? (BRAM port collisions; DSP sharing.)
  7. Synthesis warnings cleaned up? (No inferred latches or unconnected nets.)

7) Structured debug of an FSM (example)

Add a state enum and one-hot encoding; drive a 4-bit debug bus:

typedef enum logic [3:0] {IDLE=4'b0001, A=4'b0010, B=4'b0100, ERR=4'b1000} state_t;
state_t state, next;
always_ff @(posedge clk) if (rst) state<=IDLE; else state<=next;
// expose 'state' to ILA or LEDs
Enter fullscreen mode Exit fullscreen mode

Assertions:

// No illegal state
assert property (@(posedge clk) $onehot(state));
// Eventually leave IDLE when start asserted
assert property (@(posedge clk) start |-> ##[1:20] state==A);
Enter fullscreen mode Exit fullscreen mode

8) Debugging AXI/valid-ready streams

  • Rules: source holds valid and data until ready=1; sink can toggle ready freely.
  • Assertions:
// data stable under backpressure
assert property (@(posedge clk) valid && !ready |=> $stable(data));
// no transfer without both high
assert property (@(posedge clk) !(valid ^ ready) throughout transfer_event);
Enter fullscreen mode Exit fullscreen mode

9) Memory/BRAM gotchas

  • Know your read-first/write-first mode; make RTL match expected behavior.
  • Register addresses/data to meet BRAM timing; confirm with P&R reports.
  • For dual-port, guard against same-address writes; add an assertion.

10) Tooling tips

  • Linters: Verilator, AscentLint, SpyGlass—catch width, CDC, and style bugs early.
  • Fast sims: Verilator for long random runs; Questa/ModelSim for assertions & SV features.
  • Build variants: DEBUG define to enable extra checks/coverage; remove for release.

Minimal “triage kit” you can drop into any design

  • A global asserts package (handshake/FSM/FIFO checkers).
  • A debug UART (printf of counters/states/flags).
  • A top-level debug bus routed to an ILA core.
  • A stimulus library (BFMs for AXI/I2C/SPI/UART).

Top comments (0)