The interviewer turns a laptop towards you: a waveform, five or six lanes, a dozen cycles. "This block has a bug. The test says the data came out wrong. Find it." Then they watch what you do first.
You have ten minutes and no simulator. What do you say in the first thirty seconds?
They are not testing your eyesight. They want to hear the method from reading a waveform with a question and the first wrong cycle, spoken aloud:
A question first. "What should this block do, and what is its latency?" A value is only wrong against an expectation.
An expected value per lane, said slot by slot.
The first wrong slot, not the one the test reported, which is usually later.
One hop back, to the register or line that made the wrong value.
A fix and a check that would have fired at that slot.
Every sentence describes; none compares. With no expected value nothing can be called wrong, and "cycle 6" is where the symptom showed, not where it started. "Add more signals" is browsing, and "a timing issue" in a zero-delay RTL waveform is a guess (timing-only bugs).
The strong answer has the same five moves every time. Say them in this order, out loud, even when you see the bug at once:
Report. "Which word was wrong, and what was expected?"
Observe with a question. "What should this block do? I will say what I expect in each slot and compare."
Localise. "The first slot that disagrees is N, on this signal. What follows may be consequence."
Hypothesise. "Its inputs in slot N-1 were right, so the logic between is wrong. My guess is X, which also predicts Y in slot M." Then check M.
Fix and prevent. "This line, and an assertion that would have fired at slot N."
Each puzzle below is a real simulated waveform of a small design with one bug, with the spec an interviewer would give. Values are hex. Each slot is sampled just before a rising edge, so a register written at the end of slot k shows its new value in slot k+1.
Spec: pipe_add adds one to each word. A word on in_data with in_valid high in slot k must leave on out_data with out_valid high in slot k+2. Someone added a second register stage to the data path last week to meet timing. Report: the scoreboard says the first word out was 01; it expected 11.
Read each lane slot by slot from the left, counting the first slot as 0; each slot is sampled just before a rising edge.
Say the expectations: words enter in slots 1, 2 and 4, so out_valid should be high in slots 3, 4 and 6 with 11, 21 and 31. Slot 2 is the first wrong slot: out_valid is already high, and out_data holds 01, a value no input made (it is the idle input 00 plus one). Slot 3 has out_valid high with 11, and slot 5 with 01 again: every valid pulse is one slot early.
Now the trap. Read out_data alone and it looks right: 11, 21 and 31 appear in order, exactly two slots after their inputs. The data is on time; out_valid is early. One hop back: out_valid must come through as many registers as the data, and here it comes through one. The new timing stage was added to the data and not to valid. Fix: take out_valid from the second valid register. Check: an assertion that out_valid implies out_data == $past(in_data, 2) + 1, which fires at slot 2.
Spec: fifo4 holds up to four words. count is the number held, full is high when it holds four and empty when it holds none, a write while full and a read while empty are ignored, and dout shows the oldest word. Report: five words, 11 to 55, were written back to back; the reader then got 55 first, and then the FIFO said it was empty.
Read each lane slot by slot from the left, counting the first slot as 0; each slot is sampled just before a rising edge.
Writes happen at the end of slots 1 to 5 while full is low. Expected: count 1, 2, 3, 4 in slots 2 to 5, full high from slot 5, and the fifth write (55) ignored. Slot 5 is the first wrong slot: after four writes count reads 0, empty is high and full is low. Slots 2 to 4 are right.
Everything the report saw follows from slot 5. With full low, the fifth write is accepted and lands on the oldest entry, so 11 is overwritten by 55 (slot 6, dout = 55): the symptom is one slot later than the cause. After one read count is 0 again, so 22, 33 and 44 are stranded behind a FIFO that says it is empty. One hop back: 3 + 1 = 0 is two-bit arithmetic. The counter has two bits and must count to four, which needs three. full compares it with 4, which a two-bit value can never equal. Fix: size the counter as $clog2(DEPTH + 1) bits. Check: lint flags it (below), and a model compare of full and empty against a count fires at slot 5.
Spec: arb2 gives the bus to one of two masters. gnt is 1 for master 0 and 2 for master 1. A master holds req for its whole burst and raises last on its final beat; a beat moves in each slot where its master has both req and the grant. A burst must never be split by the other master. Report: the slave received master 0's three-beat burst with a beat of master 1's in the middle.
Read each lane slot by slot from the left, counting the first slot as 0; each slot is sampled just before a rising edge.
Master 0's beats move in slots 2 and 3; its last beat is the one with last0 high, so the grant must stay 1 until that beat moves. Slot 4 is the first wrong slot: gnt is 2 while master 0's third beat (last0 high) is still waiting. Master 1's single beat moves in slot 4 and master 0 finishes in slot 5: the burst was split.
One hop back: the grant changed at the end of slot 3. What in slot 3 could end a burst? Master 0's last0 was low; the only last that was high was master 1's, on a beat that had not been granted. The arbiter ended the burst on any master's last, not the owner's. Fix: end the burst on req & gnt & last. Check: an assertion that an owner with req high and last low keeps the grant in the next cycle.
In the room your answer ends with a hypothesis; here each can be proven. Each design has a FIX parameter (0 the bug, 1 the fix and default), and each bench drives random traffic, compares with a model and stops at the first disagreement.
// pipe_add: adds one to each word. A second register stage was added to the data path for timing.
// FIX = 0 is that change as first written: valid still comes from the first stage. FIX = 1 (the
// default) moves valid through both stages. The bench drives random traffic and checks every
// output word against a queue of expected words.
`timescale 1ns/1ps
module pipe_add #(parameter bit FIX = 1) (
input logic clk, rst_n,
input logic in_valid,
input logic [7:0] in_data,
output logic out_valid,
output logic [7:0] out_data
);
logic [7:0] d1, d2;
logic v1, v2;
always_ff @(posedge clk or negedge rst_n)
if (!rst_n) begin
d1 <= '0; d2 <= '0; v1 <= 1'b0; v2 <= 1'b0;
end else begin
d1 <= in_data + 8'd1; // stage 1: add one
d2 <= d1; // stage 2: the register added for timing
v1 <= in_valid;
v2 <= v1;
end
assign out_data = d2;
assign out_valid = FIX ? v2 : v1; // bad when FIX = 0: valid skips the new stage
endmodule
module tb;
parameter bit FIX = 1;
logic clk = 1'b0, rst_n = 1'b0, in_valid = 1'b0;
logic [7:0] in_data = '0;
logic out_valid;
logic [7:0] out_data;
logic [7:0] exp_q[$];
int unsigned x = 32'h1234_5678, n = 0;
always #5 clk = ~clk;
pipe_add #(.FIX(FIX)) dut (.*);
function automatic int unsigned rnd();
x ^= x << 13; x ^= x >> 17; x ^= x << 5;
return x;
endfunction
initial begin
@(negedge clk) rst_n = 1'b1;
for (int c = 1; c <= 300; c++) begin
@(negedge clk); // outputs settled after the last edge
if (out_valid) begin
if (exp_q.size() == 0) $fatal(1, "FAIL cycle=%0d: out_valid with no word expected", c);
if (out_data !== exp_q[0])
$fatal(1, "FAIL cycle=%0d: out_valid=1 with out_data=%h, expected %h", c, out_data, exp_q[0]);
void'(exp_q.pop_front()); n++;
end
in_valid = (rnd() % 3) != 0; // next input, applied before the edge
in_data = 8'(rnd());
if (in_valid) exp_q.push_back(in_data + 8'd1);
end
$display("PASS: 300 cycles, %0d words checked, every one in step with valid (FIX=%0d)", n, FIX);
$finish;
end
endmodule// fifo4: a four-entry synchronous FIFO with an occupancy counter. FIX = 0 declares the counter
// two bits wide, enough for 0 to 3 but not 4; FIX = 1 (the default) makes it three bits. The bench
// drives random writes and reads and checks full, empty and every word read against a model.
`timescale 1ns/1ps
module fifo4 #(parameter bit FIX = 1) (
input logic clk, rst_n,
input logic wr_en, rd_en,
input logic [7:0] din,
output logic [7:0] dout,
output logic full, empty
);
localparam int CW = FIX ? 3 : 2; // bad when FIX = 0: two bits cannot hold 4
logic [7:0] mem [4];
logic [1:0] wp, rp;
logic [CW-1:0] count;
wire do_wr = wr_en && !full;
wire do_rd = rd_en && !empty;
always_ff @(posedge clk or negedge rst_n)
if (!rst_n) begin
wp <= '0; rp <= '0; count <= '0;
end else begin
if (do_wr) begin mem[wp] <= din; wp <= wp + 2'd1; end
if (do_rd) rp <= rp + 2'd1;
count <= count + CW'(do_wr) - CW'(do_rd);
end
assign full = (count == 4);
assign empty = (count == 0);
assign dout = mem[rp];
endmodule
module tb;
parameter bit FIX = 1;
logic clk = 1'b0, rst_n = 1'b0, wr_en = 1'b0, rd_en = 1'b0;
logic [7:0] din = '0, dout;
logic full, empty;
logic [7:0] model[$];
int unsigned x = 32'hC0FF_EE01, nrd = 0, nfull = 0, held;
always #5 clk = ~clk;
fifo4 #(.FIX(FIX)) dut (.*);
function automatic int unsigned rnd();
x ^= x << 13; x ^= x >> 17; x ^= x << 5;
return x;
endfunction
initial begin
@(negedge clk) rst_n = 1'b1;
for (int c = 1; c <= 400; c++) begin
@(negedge clk); // state settled after the last edge
if (full !== (model.size() == 4) || empty !== (model.size() == 0))
$fatal(1, "FAIL cycle=%0d: full=%b empty=%b with %0d words held", c, full, empty, model.size());
if (full) nfull++;
wr_en = (rnd() % 4) < 3; // writes outnumber reads, so it fills
rd_en = (rnd() % 4) < 1;
din = 8'(rnd());
held = model.size(); // the model updates as the next edge will
if (rd_en && held != 0) begin
if (dout !== model[0]) $fatal(1, "FAIL cycle=%0d: read %h, expected %h", c, dout, model[0]);
void'(model.pop_front()); nrd++;
end
if (wr_en && held < 4) model.push_back(din);
end
$display("PASS: 400 cycles, %0d reads checked, full for %0d cycles (FIX=%0d)", nrd, nfull, FIX);
$finish;
end
endmodule// arb2: a two-master round-robin arbiter for bursts. Each master holds req for its whole burst and
// raises last on its final beat; a burst must not be split. FIX = 0 ends the owner's grant when ANY
// master shows last; FIX = 1 (the default) only on the owner's last beat. The bench runs random
// bursts and checks that a grant never moves in the middle of a burst and every burst finishes.
`timescale 1ns/1ps
module arb2 #(parameter bit FIX = 1) (
input logic clk, rst_n,
input logic [1:0] req, // per master: a beat is waiting
input logic [1:0] last, // per master: that beat is the last of its burst
output logic [1:0] gnt // one-hot owner; a beat moves when req & gnt
);
logic nxt; // the master that wins a tie
logic [1:0] cand;
wire [1:0] beat = req & gnt;
wire done = FIX ? |(beat & last)
: |(req & last); // bad when FIX = 0: a waiting master's last counts
always_comb begin
cand = req & ~(done ? gnt : 2'b00); // a master that just finished does not win again at once
if (gnt != 2'b00 && !done) cand = 2'b00; // the owner keeps the bus mid-burst
end
always_ff @(posedge clk or negedge rst_n)
if (!rst_n) begin
gnt <= 2'b00; nxt <= 1'b0;
end else if (gnt == 2'b00 || done) begin
if (cand[nxt]) begin gnt <= 2'b01 << nxt; nxt <= !nxt; end
else if (cand[!nxt]) begin gnt <= 2'b01 << !nxt; nxt <= nxt; end
else gnt <= 2'b00;
end
endmodule
module tb;
parameter bit FIX = 1;
logic clk = 1'b0, rst_n = 1'b0;
logic [1:0] req = '0, last = '0, gnt;
int left [2] = '{0, 0}; // beats left in each master's burst
bit mid [2] = '{0, 0}; // a burst has started (moved at least one beat)
int wait_c [2] = '{0, 0};
int unsigned x = 32'h0BAD_5EED, bursts = 0;
always #5 clk = ~clk;
arb2 #(.FIX(FIX)) dut (.*);
function automatic int unsigned rnd();
x ^= x << 13; x ^= x >> 17; x ^= x << 5;
return x;
endfunction
initial begin
@(negedge clk) rst_n = 1'b1;
for (int c = 1; c <= 500; c++) begin
@(negedge clk);
if (gnt == 2'b11) $fatal(1, "FAIL cycle=%0d: gnt=11, two owners", c);
for (int m = 0; m < 2; m++) begin
if (mid[m] && !gnt[m]) $fatal(1, "FAIL cycle=%0d: master %0d lost the grant in the middle of a burst (%0d beat(s) left)", c, m, left[m]);
wait_c[m] = (req[m] && !gnt[m]) ? wait_c[m] + 1 : 0;
if (wait_c[m] > 12) $fatal(1, "FAIL cycle=%0d: master %0d waited 12 cycles", c, m);
end
// each master's request for the next edge, then the beats that edge moves
for (int m = 0; m < 2; m++) begin
if (left[m] == 0 && (rnd() % 4) == 0) left[m] = 1 + rnd() % 4; // a new burst of 1 to 4 beats
req[m] = (left[m] != 0);
last[m] = (left[m] == 1);
if (req[m] && gnt[m]) begin
left[m]--; mid[m] = (left[m] != 0);
if (left[m] == 0) bursts++;
end
end
end
$display("PASS: 500 cycles, %0d bursts, none split, no master waited over 12 cycles (FIX=%0d)", bursts, FIX);
$finish;
end
endmodule$ iverilog -g2012 -P tb.FIX=0 -o pipe0.vvp pipe.sv && vvp -n pipe0.vvp | grep -E '^(FATAL|PASS)'
FATAL: pipe.sv:49: FAIL cycle=2: out_valid=1 with out_data=01, expected a4
$ iverilog -g2012 -P tb.FIX=1 -o pipe1.vvp pipe.sv && vvp -n pipe1.vvp | grep -E '^(FATAL|PASS)'
PASS: 300 cycles, 203 words checked, every one in step with valid (FIX=1)
$ iverilog -g2012 -P tb.FIX=0 -o fifo0.vvp fifo.sv && vvp -n fifo0.vvp | grep -E '^(FATAL|PASS)'
FATAL: fifo.sv:49: FAIL cycle=6: full=0 empty=1 with 4 words held
$ iverilog -g2012 -P tb.FIX=1 -o fifo1.vvp fifo.sv && vvp -n fifo1.vvp | grep -E '^(FATAL|PASS)'
PASS: 400 cycles, 93 reads checked, full for 268 cycles (FIX=1)
$ iverilog -g2012 -P tb.FIX=0 -o arb0.vvp arb.sv && vvp -n arb0.vvp | grep -E '^(FATAL|PASS)'
FATAL: arb.sv:51: FAIL cycle=36: master 1 lost the grant in the middle of a burst (1 beat(s) left)
$ iverilog -g2012 -P tb.FIX=1 -o arb1.vvp arb.sv && vvp -n arb1.vvp | grep -E '^(FATAL|PASS)'
PASS: 500 cycles, 146 bursts, none split, no master waited over 12 cycles (FIX=1)
$ verilator --binary --timing --assert -Wno-fatal --top-module tb -GFIX=0 -Mdir vl_fifo fifo.sv > /dev/null 2>&1; vl_fifo/Vtb | grep Fatal
[70000] %Fatal: fifo.sv:49: Assertion failed in tb.unnamedblk1: FAIL cycle=6: full=0 empty=1 with 4 words held
$ verilator --lint-only -Wall -GFIX=0 --top-module fifo4 fifo.sv
%Warning-DECLFILENAME: fifo.sv:5:8: Filename 'fifo' does not match MODULE name: 'fifo4'
...
%Warning-WIDTHEXPAND: fifo.sv:26:25: Operator EQ expects 32 or 3 bits on the LHS, but LHS's VARREF 'count' generates 2 bits.
...
%Error: Exiting due to 2 warning(s)Each bug fails at its first chance: pipe_add at the first valid pulse, fifo4 the first time it holds four words, arb2 the first time a waiting master asks for one beat during another's burst. Verilator stops at the same cycles. With FIX = 1 all three pass hundreds of random cycles. The lint line is worth saying in the room: Verilator points at line 26, the comparison with 4, without simulating.
In all three puzzles the report and the first wrong slot differed. Puzzle 1's scoreboard blamed data that was on time; puzzle 2's reader saw the wrong word a slot after the cause; puzzle 3's burst was split by a decision made the slot before the visible change. The first wrong slot is the only one you can reason backwards from.
"There is no spec." Ask for the intent in one sentence, then check invariants any such design must keep: a FIFO never loses words, valid and data move together, grants are one-hot.
"The dump is two million cycles long." Do not scroll: halve the window (bisection in time) or let a script find the first difference (comparing a good and a bad run).
"Is it the design or the testbench?" Compare both with the spec; if the bench's expectation is what disagrees, the bug is in the test (when the bug is in the test).
"How would you be sure of your fix?" Fail without it, pass with it, in two simulators, with an assertion on the rule silent: see how do you know your fix works?
Because in slot 3 every lane still has the value the spec asks for: the owner has the grant and its second beat moves. The decision taken at the end of slot 3 first shows in slot 4. That is the normal shape of a registered bug: the first wrong value appears one slot after the inputs that caused it, so you name slot 4, then look at slot 3's inputs to explain it.
Waveforms: COURSE=dbg node scripts/tools/gen-waves.mjs interview-prep:find-the-bug-in-this-waveform (Icarus Verilog 13.0, 2026-10-18) on wave-src/find-the-bug-in-this-waveform/pipe-valid.sv, fifo-count.sv and arb-burst.sv, each the lesson's module with FIX = 0 and directed stimulus, sampled just before each rising edge. All three matched their expectations and a second run changed nothing.
Benches as shown: iverilog -g2012 -P tb.FIX=0 then vvp -n fails pipe_add at cycle 2, fifo4 at cycle 6 and arb2 at cycle 36; -P tb.FIX=1 passes all three (203 words, 93 reads, 146 bursts). verilator --binary --timing --assert -Wno-fatal --top-module tb -GFIX=0 (Verilator 5.048) stops each at the same cycle with the same message. verilator --lint-only -Wall -GFIX=0 --top-module fifo4 fifo.sv reports WIDTHEXPAND at line 26; with -GFIX=1 only DECLFILENAME.
Limits: three small designs with one bug each, in simulation. A real interview waveform is often a screenshot with no bench; the method is the same, the proof is not available to you in the room. The interviewer dialogue is illustrative.