Skip to content

FPGA & HDL interview questions

Describing hardware and closing timing on it.

Practice all 10, gradedOne at a time. Free, no account.

What interviewers probe

  • RTL Design
  • Timing Closure
  • Simulation & Testbenches
  • FPGA Resources

Builds on Sequential Logic & State Machines, Setup & Hold Timing.

10 questions, easiest first

  1. 1You write an always block in Verilog to build an 8-bit shift register using blocking assignments (=) instead of non-blocking assignments (<=) for all the internal stage transfers. In simulation the output looks wrong -- it behaves like a single flip-flop instead of an 8-stage shift register. Explain exactly why this happens and how you'd rewrite it.intro

    The block looks like: always @(posedge clk) begin q[0] = d; q[1] = q[0]; q[2] = q[1]; ... q[7] = q[6]; end

    Answer this one, graded
    Or just show what a strong answer covers

    Because these are blocking assignments, each statement fully executes -- including updating the target -- before the next statement runs, even though they're all inside the same posedge block. So q[0]=d updates q[0] immediately, and then q[1]=q[0] reads that brand-new value, not the value q[0] held before the clock edge. That collapses the whole chain so d appears to walk through all eight stages in a single clock edge instead of one stage per edge -- effectively it behaves like one register with combinational-looking propagation. Real flip-flops all latch their D input based on the value present right before the clock edge, in parallel. Non-blocking assignments (<=) model that correctly: all the RHS values are read first, then all targets update simultaneously at the end of the time step. The fix is simply changing every = to <= in that block so q[1] gets the old q[0], q[2] gets the old q[1], and so on.

    • Identifies blocking assignment as the cause — States that q[0]=d executes first and its new value immediately feeds q[1]=q[0] in the same clock edge, collapsing all stages into one cycle of delay.
    • Explains correct fix — Proposes replacing = with <= for all stage assignments so every register samples the pre-edge value simultaneously.
    • Connects to real hardware behavior — Notes that real flip-flops all sample their D input at the same clock edge in parallel, and non-blocking assignments are the construct that models that.

    Where answers go wrong: Saying blocking assignments are 'just wrong syntax' without explaining the sequential-execution mechanism. · Confusing this with a combinational-logic sensitivity list issue rather than a sequential update-ordering issue. · Claiming synthesis would 'fix it automatically' -- many tools will synthesize exactly the buggy same-cycle behavior.

  2. 2You write a combinational always block for a priority encoder: `always @(*) begin if (a) out = 2'b00; else if (b) out = 2'b01; else if (c) out = 2'b10; end` -- with no final else. It simulates correctly for every case you tried where at least one of a, b, c is high. But when you load the bitstream, the board misbehaves whenever a=b=c=0, and the synthesis log has a warning you initially ignored. What is actually happening, and how would you track it down?intro
    Answer this one, graded
    Or just show what a strong answer covers

    Because there's no else covering a=b=c=0, out isn't assigned in that branch, so the synthesis tool has to infer memory to hold whatever value it last had -- a latch, not a wire. In simulation, if 'out' was initialized to something and the testbench never held a=b=c=0 for long, or the register just kept its last driven value, it looks fine. On hardware there's no guaranteed reset state for that latch and its timing behavior is completely different from combinational logic, so it can glitch or hold stale data. I'd go back and check the synthesis log for a 'latch inferred' warning -- that's the standard signal. To fix it, I'd add a default assignment like out = 2'b11 at the top of the always block, or add an explicit else, so every path assigns 'out' and the tool builds pure combinational logic instead.

    • Identifies inferred latch — States that the missing else branch means 'out' has no assigned value for a=b=c=0, so the tool infers a latch that holds its previous state instead of pure combinational logic.
    • Explains sim/hardware mismatch — Explains why simulation might mask this (e.g., initial value, or a testbench that never actually exercised the all-zero case) while hardware exposes it.
    • Names concrete debug step — Mentions checking synthesis/elaboration warnings or the schematic viewer for a latch, not just re-running simulation with more vectors.
    • Gives correct fix — States the fix: add a default assignment at the top of the block or an else covering all cases so out is always driven.

    Where answers go wrong: Blaming a clock domain crossing or metastability instead of latch inference. · Assuming it's a synthesis tool bug rather than an RTL coding issue. · Not realizing simulation initial values can hide the missing-else bug entirely.

  3. 3A design runs correctly for hours in RTL simulation with a single clock, but on the actual board a signal generated in one clock domain is sampled directly by logic in a second, faster clock domain with no synchronizer. The board occasionally glitches -- wrong counts, missed events -- but never in simulation. What's happening, and how would you confirm it on the bench versus just assuming?easy
    Answer this one, graded
    Or just show what a strong answer covers

    This looks like a classic clock-domain-crossing metastability issue. If a signal generated in one clock domain is sampled directly by a register clocked from a different, asynchronous clock, there's no guarantee the signal is stable during that destination flop's setup/hold window. Occasionally the transition lands right on the edge and the flop goes metastable, resolving to a random 0 or 1 or taking extra time to settle, which then propagates as a corrupted value downstream -- explaining the occasional wrong counts. Simulation never shows this because RTL simulators are event-driven and don't model the analog settling behavior of a real flip-flop; a signal transition just resolves cleanly at some simulated time. To confirm on the bench, I'd add a proper two-flop synchronizer on that crossing and see if the glitch rate drops to zero, and/or use an ILA to capture the signal relative to both clock edges and look for occurrences where the async signal changes within one destination clock period of the sampling edge, correlating those with the observed glitches.

    • Names clock-domain crossing without synchronizer as the root cause — Explicitly states the signal crosses domains without a two-flop (or better) synchronizer, causing setup/hold violations at the destination flop.
    • Explains why it's intermittent — Connects the failure rate to how often the source transition lands near the destination clock edge -- a probabilistic/statistical phenomenon, not a deterministic bug.
    • Explains why simulation doesn't show it — States that standard RTL simulation doesn't model analog metastable settling time, so the bug is invisible until hardware.
    • Proposes a bench confirmation method — Suggests a concrete check such as adding a synchronizer and re-testing, or using an on-chip logic analyzer / ILA to capture the async signal relative to both clocks and look for glitches correlated with edge proximity.

    Where answers go wrong: Blaming it on 'noise' or power supply issues without considering the clock domain structure. · Assuming simulation would have caught a real timing violation. · Proposing to 'just add a synchronizer' without explaining why it fixes the underlying probability of metastable resolution propagating downstream.

  4. 4Your RTL multiplies two 16-bit signals every clock cycle inside a small filter block. After synthesis, the resource utilization report shows 0% DSP block usage but LUT usage has jumped to nearly 40% of the device. What likely happened, and what would you check to fix it?easy
    Answer this one, graded
    Or just show what a strong answer covers

    Zero percent DSP usage with a big LUT jump tells me the multiplier didn't get mapped to the dedicated DSP hardware and got built out of general logic instead -- LUTs implementing partial products and adders, which is both huge and slow compared to a hardened multiplier. Likely causes are things like unusual or mismatched operand widths, mixing the multiply with other arithmetic in the same expression so the tool can't cleanly recognize the multiplier pattern, signed/unsigned mismatches, or a synthesis attribute/option that disabled DSP inference for that block. I'd start by checking the synthesis log for an explicit inference message about that multiply -- most tools tell you when they decline to use a DSP and often why. Then I'd isolate the multiply into its own clean, registered statement matching the tool's expected inference template, or explicitly instantiate the DSP primitive if I need it guaranteed. After the change I'd re-run synthesis and confirm DSP utilization actually goes up and LUT usage drops back down.

    • Identifies the DSP not being used as the core issue — States plainly that the multiply is being implemented in fabric/LUTs instead of the dedicated DSP block, which is why LUT usage exploded.
    • Gives a plausible cause — Names at least one realistic reason inference failed: mismatched or unusual bit widths, combining the multiply with other arithmetic in the same expression, unsupported signed/unsigned mixing, or a tool setting/attribute disabling DSP mapping.
    • Describes a concrete check/fix — Proposes checking the synthesis log for inference messages, isolating the multiply into its own registered statement, or explicitly instantiating/attribute-forcing the DSP primitive, then re-checking the utilization report.

    Where answers go wrong: Assuming the device simply ran out of DSPs without checking the report, which explicitly shows 0% used. · Jumping straight to manually instantiating a DSP primitive without first checking why inference failed, which wastes portability. · Not mentioning re-checking the synthesis report/log after the fix to confirm the mapping actually changed.

  5. 5You need a 1024-deep, 8-bit-wide lookup table in your RTL, so you declare `reg [7:0] mem [0:1023];`, write to it in one always block, and read from it combinationally elsewhere in the same module. After synthesis, the resource utilization report shows thousands of extra LUTs and flip-flops consumed, and zero block RAM used. Why did the tool build this out of fabric logic instead of using the FPGA's dedicated memory blocks, and what would you check or change?easy
    Answer this one, graded
    Or just show what a strong answer covers

    Block RAM primitives on the FPGA have a registered output -- you present an address on a clock edge and the data comes out registered, one cycle later. My RTL reads the array combinationally, expecting the data in the same cycle the address changes, which doesn't match any BRAM template the tool knows how to map to. So instead of failing, it falls back to building the whole 1024x8 array out of flip-flops and LUT-based multiplexers, which is why utilization exploded and no BRAM shows up. I'd check the synthesis inference report to confirm that's the reason, then rewrite the read path to register the address and output -- read becomes a synchronous process -- so the structure matches what the tool expects for BRAM inference, and re-check the utilization report afterward to confirm it dropped in.

    • Identifies async read as root cause — States that reading the array combinationally, without a clocked output register, is why the tool couldn't map it to a block RAM primitive.
    • Explains BRAM structure — Explains that dedicated block RAM has a synchronous/registered output, so RTL that expects same-cycle combinational read doesn't match the required template.
    • Names concrete fix — Proposes rewriting the read as a registered process (read address latched, data appears one cycle later) to enable inference.
    • Mentions checking tool feedback — Mentions consulting the synthesis inference/mapping report or messages to see explicitly why BRAM wasn't chosen, rather than only guessing.

    Where answers go wrong: Assuming the tool simply failed or has a bug rather than following documented inference rules. · Not knowing the difference between synchronous and asynchronous memory reads. · Suggesting a vendor-specific IP/attribute without first fixing the underlying RTL structure.

  6. 6A timing report shows a register-to-register path with: clock-to-out delay of the source flip-flop = 0.6 ns, combinational logic + routing delay = 3.8 ns, destination flip-flop setup time = 0.4 ns, and clock uncertainty (skew + jitter budget) = 0.3 ns. Your design's clock constraint requires a 200 MHz clock (5.0 ns period). Compute the actual maximum achievable frequency for this path and state whether the 200 MHz constraint is met.medium

    Use: required arrival time = period - setup - uncertainty; the path fails if data arrival time (Tco + logic delay) exceeds that.

    Answer this one, graded
    Or just show what a strong answer covers

    The total delay the data has to clear in one clock period is Tco plus the logic/routing delay plus destination setup plus clock uncertainty: 0.6 + 3.8 + 0.4 + 0.3 = 5.1 ns. That means this path can only run reliably at 1/5.1ns, which is about 196 MHz. The design was constrained to 200 MHz, i.e. a 5.0 ns period, so this path needs 5.1 ns but only gets 5.0 ns -- it fails timing with roughly -0.1 ns of slack. So no, the 200 MHz constraint is not met; this path is the bottleneck and needs to be sped up or the clock needs to be relaxed.

    • Sums the correct delay terms — Adds Tco + logic delay + setup + uncertainty (0.6+3.8+0.4+0.3 = 5.1 ns) as the effective required period for this path.
    • Computes Fmax correctly — Converts the 5.1 ns period to approximately 196 MHz (1/5.1ns).
    • Correctly judges the constraint — States that 200 MHz (5.0 ns) is NOT met since 5.1 ns > 5.0 ns, i.e., the path has negative slack of about -0.1 ns.

    Where answers go wrong: Forgetting to include clock-to-out delay or clock uncertainty in the sum, understating the true path delay. · Confusing slack sign convention and declaring the path passes when the computed period exceeds the requested period. · Computing Fmax from only the combinational delay term without the flop and uncertainty overheads.

  7. 7Your testbench achieves what you consider full functional coverage and passes cleanly in simulation. When you load the same bitstream onto the board, the design fails right after power-up but then, if you press the manual reset button, it runs correctly from then on. What would you suspect first, and how would you verify it without just guessing?medium
    Answer this one, graded
    Or just show what a strong answer covers

    The symptom -- fails at power-up, fixed permanently by a manual reset -- points squarely at the reset architecture, not the steady-state logic, since a logic bug wouldn't be cured by pressing reset once. My first suspicion is that either there's no proper power-on reset pulse being generated and released cleanly, or the reset release isn't synchronized to the clock and some registers come out of configuration or power-up in a state the design didn't account for. The reason simulation never caught this is that testbenches almost always explicitly assert a clean reset at time zero before exercising the design, so the design's actual startup state is never really tested -- and register initial values assumed by the simulator don't necessarily match what real silicon does before an explicit reset pulse arrives. To verify, I'd first check the reset generation and synchronization logic and confirm there's a proper reset pulse with adequate width applied at power-up, ideally scoping the reset net during power-up. I'd also modify the testbench to start with no explicit reset (or with registers forced to a genuinely undefined/worst-case start state) and see if the failure reproduces in simulation, which would confirm the root cause before touching hardware again.

    • Identifies power-up initial state / reset as the suspect — Points to the design's reset architecture or initial register state as the likely cause, not a logic bug in the steady-state function.
    • Explains the simulation vs hardware discrepancy — Explains that simulation initializes registers to known/defined values (e.g., via initial blocks or default state assumptions) whereas real hardware power-up state can differ or be undefined until reset asserts, so a reset-dependent bug is invisible in sim.
    • Proposes a verification method — Suggests a concrete test such as removing all initial-value assumptions from the testbench and re-simulating with X's/undefined start states, or checking whether a proper reset pulse is generated and synchronized at power-up, e.g. with a scope/ILA on the reset net.

    Where answers go wrong: Assuming it's a clock stability or PLL lock issue without first ruling out reset behavior, even though the fix-by-manual-reset symptom points directly at reset. · Not questioning why the testbench 'passed' -- most testbenches implicitly apply a clean reset pulse at time zero and never test the power-on-without-reset case. · Proposing to just add more delay before deasserting reset without first confirming that's actually where the bug lives.

  8. 8After place-and-route, your timing report shows setup timing comfortably met with 2 ns of margin at your target frequency, but the same report flags a hold violation of 0.15 ns on a path between two adjacent flip-flops, FF_A driving FF_B directly through a single buffer with almost no logic delay. The report shows clock insertion delay to FF_A is 0.3 ns and to FF_B is 0.6 ns. Explain what's actually being violated here and why it has nothing to do with your clock frequency, then describe how you'd confirm and fix it.medium
    Answer this one, graded
    Or just show what a strong answer covers

    Hold violations check that data stays stable long enough after a clock edge for the next flop to reliably capture it -- it's a minimum-delay requirement on the same clock edge, so it doesn't care what my clock period is; making the clock slower does nothing to fix it. Here the insertion delay to FF_B is 0.6 ns versus 0.3 ns to FF_A, so FF_B's clock edge effectively arrives 0.3 ns 'early' relative to FF_A's launch edge from FF_B's perspective, eating into the margin between the data changing and the hold window closing. With almost zero combinational delay in between, that skew alone is enough to blow the 0.15 ns hold margin. I'd confirm by pulling up the detailed hold path report to see the launch/capture insertion delays and combinational delay add up the way the summary implies, then fix it either by letting the tool auto-insert small hold-fix delay buffers on that specific path, or manually adding delay, or looking at why the clock tree is that skewed between two adjacent flops and improving placement/buffering there.

    • Distinguishes hold from setup — Explicitly states hold violations are a minimum-delay/same-edge check independent of clock period, so raising or lowering frequency won't fix it.
    • Identifies skew as the cause — Points to the 0.3 ns clock insertion delay mismatch between FF_A and FF_B as effectively advancing FF_B's capture edge and eating the hold margin.
    • Describes confirmation method — Mentions inspecting the hold timing report/path details or clock tree report to confirm the skew and path delay numbers add up to the reported violation.
    • Gives a concrete fix — Proposes at least one concrete fix: adding data-path delay (buffers), letting the tool auto-insert hold fix cells, or adjusting placement/clock buffering to reduce skew.

    Where answers go wrong: Trying to fix a hold violation by lowering the clock frequency, which has no effect since hold is period-independent. · Confusing hold violation direction with setup violation direction. · Not connecting the clock insertion delay numbers given in the prompt to the actual mechanism of the violation.

  9. 9A timing report shows your critical path is negative by 1.2 ns at a 250 MHz target: it runs through a wide 4-to-1 multiplexer selecting one of four 32-bit operands, feeding directly into a 32-bit adder, then into a register. You've already tried letting the tool re-place-and-route with higher effort and it didn't help. Walk me through at least two structurally different ways you'd close this timing violation, and what each costs you.hard
    Answer this one, graded
    Or just show what a strong answer covers

    The critical path is long because it's doing two full operations in series within one clock period -- a 4-way select across 32 bits, then a full 32-bit add with its carry chain -- so my first move is retiming/pipelining: insert a register right after the mux, so the select happens in one cycle and the add happens in the next. That directly cuts the combinational delay roughly in half, at the cost of one extra cycle of latency through this stage, which is fine if the surrounding logic can absorb an extra pipeline cycle but painful if this sits inside a tight single-cycle feedback loop. A structurally different fix is to restructure the logic itself rather than add a cycle: for example, push the mux selection earlier so each of the four operands is added to its partner speculatively in parallel and only the final result is selected after the adds, effectively hiding the mux behind the adder's delay instead of putting it in series -- this costs extra adder hardware (four adders instead of one) but doesn't add latency. A third option, if operand widths allow it, is switching the adder architecture to something with a shorter critical delay like a carry-select or carry-lookahead structure, which costs extra LUTs/area for the lookahead logic but keeps single-cycle timing. I'd pick based on whether the design can tolerate an extra pipeline cycle, and whether I have area headroom to spend instead.

    • Proposes pipelining/retiming as one fix — Suggests registering the mux output (or otherwise adding a pipeline stage) to split the path into two shorter combinational segments, explicitly naming the added latency cycle cost.
    • Proposes a structurally different second fix — Offers a genuinely different approach such as restructuring/reordering the logic (e.g., moving the select earlier, using a faster adder architecture, or reducing operand width if possible) rather than two variations on the same idea.
    • States concrete costs, not just benefits — For each proposed fix, names a specific tradeoff: extra latency cycle, extra registers/area, increased design/verification complexity, or reduced flexibility.
    • Grounds reasoning in the specific critical path — References the actual structure given -- mux feeding an adder -- rather than giving generic 'add pipeline stages' advice with no connection to why this path is long (fan-in from 4-way select plus full adder carry chain in series).

    Where answers go wrong: Only mentioning 'add a pipeline stage' as the answer with no discussion of the resulting latency or throughput impact. · Suggesting only synthesis/PnR settings (higher effort, different strategy) after being told that's already been tried and didn't work. · Ignoring that the specific critical path is two full operations in series (select then add) and not addressing why that's the actual bottleneck.

  10. 10Your 32-bit multiply-accumulate chain is the critical path, failing timing by 1.5 ns at your target frequency. You're considering inserting a pipeline register in the middle of the multiplier to break the path in half. The problem: the FSM that issues operands to this block currently assumes the result is valid on the very next clock cycle, and that same FSM also handles bus arbitration for other requesters in a tight state sequence. Walk through the tradeoffs of adding this pipeline stage and what you'd actually have to change to make it correct.hard
    Answer this one, graded
    Or just show what a strong answer covers

    Pipelining trades latency for frequency: cutting the multiplier's combinational path in half with a register should roughly halve that portion of the delay and let me hit the target frequency, but the result is now available one cycle later than before. That breaks the FSM's assumption that it can read the result on the very next cycle, so I have to add an extra wait state, or better, have the multiplier assert a 'result_valid' signal the FSM waits on instead of hardcoding a cycle count -- that's more robust if the pipeline depth ever changes again. Since this FSM also handles bus arbitration in the same sequence, I need to make sure the extra wait cycle doesn't starve other requesters or introduce priority bugs -- I'd probably want to keep it to a single in-flight multiply operation for now rather than trying to pipeline multiple requests, to avoid a hazard where a second operand set gets issued before the first result is captured. I'd also make sure the register goes at the point in the multiplier tree that actually balances delay on both sides -- not just wherever's convenient -- and then re-run functional simulation with the new latency modeled, plus re-check the timing report, before trusting either number.

    • States the core tradeoff — Explicitly frames this as trading one (or more) cycles of latency for a shorter critical path / higher achievable frequency, not as a free fix.
    • Identifies FSM correctness break — Explains that the FSM's next-cycle assumption is now wrong and must be updated with additional wait states or a valid/done signal from the multiplier.
    • Addresses hazard/back-to-back issue — Raises the concern of a new operation being issued while a previous one is still in the pipeline and proposes a way to prevent or handle it (e.g., stall arbitration, single-in-flight restriction, valid tagging).
    • Notes placement of the pipeline cut matters — Mentions that the register must be inserted at a point that actually balances delay between the two resulting halves, not just anywhere in the multiplier.
    • Flags re-verification need — States that RTL and testbench changes require re-running functional simulation (and re-checking timing) since behavior, not just timing, has changed.

    Where answers go wrong: Treating pipelining as a purely timing fix with no functional consequence to the FSM. · Forgetting that bus arbitration logic sharing the same FSM state sequence is now also affected by the stall/wait cycles. · Assuming any register insertion point in the multiplier is equally effective for closing timing. · Not mentioning that simulation/testbenches must be updated to check for the new latency, not just re-checking timing reports.

Next topicControl Systems

Interviewing for a specific role?

Paste the job description and we weight fpga & hdl against everything else the role needs.