“Here is a block you have never seen. Review it in 30 minutes and tell us what you find.”
The question tests triage, not completeness. The signal is a fixed pass order that finds load-bearing defects first and says what was skipped.
Work from the outside in. Interfaces first, then control, then reset and clocks, then datapath timing, then verification hooks. Each pass has one question, one typical defect and a fixed slice of the 30 minutes.
Read it top to bottom; when a box's minutes run out, note what is unchecked and move on.
The boundary passes come first because their defects cost the most: widths, protocols, resets and crossings.
The report has its own 3 minutes. A finding that is never written down does not exist.
Pass | Question | Typical defect |
|---|---|---|
Interfaces | What crosses the boundary, with what protocol and whose clock | Undriven output, truncated width, undocumented backpressure |
Control | Is every state reachable and every stall escapable | Missing default transition, stall that never releases |
Reset and clocks | What style, what release order, what crossings | Unsynchronized release, undocumented crossing |
Datapath and timing | Where is the combinational depth, what infers storage | Inferred latch, deep unregistered mux |
Verification hooks | How would a waveform show the bug | No assertion or status flag on the key invariant |
Timebox each pass. A review that spends twenty minutes on the ALU and never checks the interfaces has failed the exercise.
The block below counts bytes on a valid/ready packet stream. It is about 20 lines long and has four seeded bugs, one for each of the first four passes. Try the passes yourself before reading the findings.
// yosys: latch
// Review target: a byte counter for a valid/ready packet stream. Four bugs are seeded.
module pkt_stat (
input logic clk, rst_n,
input logic in_valid,
input logic [7:0] in_len,
output logic in_ready,
input logic clr, // from the CPU clock domain
output logic [7:0] total // bytes seen since the last clear
);
typedef enum logic [1:0] {IDLE, ACC, HOLD} st_t;
st_t st;
logic [7:0] len_q;
always_ff @(posedge clk or negedge rst_n)
if (!rst_n) begin st <= IDLE; total <= '0; end
else case (st)
IDLE: if (in_valid) begin len_q <= in_len; st <= ACC; end
ACC: begin total <= total + len_q; st <= HOLD; end
HOLD: if (clr) begin total <= '0; st <= IDLE; end
endcase
always_comb
if (st == IDLE) in_ready = 1'b1;
endmoduleEach finding is written the way it would go in the report: location, trigger, consequence, then the fix.
Pass (minutes) | Finding | Fix |
|---|---|---|
1. Interfaces (0 to 6) | total is 8 bits but adds 8-bit lengths. Trigger: more than 255 bytes since the last clear; 200 + 200 reads 144. Consequence: the count wraps with no flag. | Size total from the spec (16 bits here), or saturate and raise an overflow flag. |
2. Control (6 to 12) | HOLD leaves only on clr. Trigger: the first packet. Consequence: the block accepts one packet, then never again until software clears it. | ACC returns to IDLE. |
3. Reset and clocks (12 to 18) | clr comes from the CPU clock domain and drives the state machine raw. Trigger: clr changing near a clk edge. Consequence: st and total can update inconsistently. | Two-flop synchroniser on clr, held long enough to be seen, or a handshake. |
4. Datapath and timing (18 to 24) | always_comb assigns in_ready only in IDLE, so it infers a latch. Trigger: leaving IDLE. Consequence: in_ready stays 1 while busy, so words offered in ACC are dropped. | Drive in_ready with a continuous assignment from st, so every path assigns it. |
5. Hooks (24 to 27) | No defect, but no assertion that total never wraps or that a handshake only happens in IDLE. | Recommend both as assertions. |
Read down the first column: each pass found the bug it is designed to find, and the fifth pass found a gap rather than a bug. Two things were looked at and deliberately not reported.
len_q has no reset value. It is written in IDLE before ACC reads it, so the value is never observed. Not a finding.
The state encoding and the names are style. Skimmed and said so.
The same block after the review, with a bench that checks each fix:
// The same block after the review: each fix is marked with its finding number.
module pkt_stat (
input logic clk, rst_n,
input logic in_valid,
input logic [7:0] in_len,
output logic in_ready,
input logic clr_async, // level from the CPU clock domain, held for at least 3 clk cycles
output logic [15:0] total // fix 1: width from the spec (here 16 bits), not from in_len
);
typedef enum logic [1:0] {IDLE, ACC} st_t;
st_t st;
logic [7:0] len_q;
logic [1:0] clr_ff; // fix 3: two-flop synchroniser on the crossing
logic clr;
always_ff @(posedge clk or negedge rst_n)
if (!rst_n) clr_ff <= 2'b00;
else clr_ff <= {clr_ff[0], clr_async};
assign clr = clr_ff[1];
always_ff @(posedge clk or negedge rst_n)
if (!rst_n) begin
st <= IDLE; total <= '0; len_q <= '0;
end else if (clr) begin
st <= IDLE; total <= '0;
end else begin
case (st)
IDLE: if (in_valid) begin len_q <= in_len; st <= ACC; end
ACC: begin total <= total + len_q; st <= IDLE; end // fix 2: back to IDLE, no stall
default: st <= IDLE;
endcase
end
assign in_ready = (st == IDLE) && !clr; // fix 4: continuous assignment, no latch
endmodule
Interfaces: every output driven; widths match the spec at both ends; the protocol and its clock named for each port.
Control: every state reachable; every wait has an exit that does not depend on software; a default arm.
Reset and clocks: every input from another clock synchronised or handshaken; reset release synchronised per domain.
Datapath and timing: no latch (every combinational output assigned on every path); the deepest unregistered path named.
Hooks: an assertion or status flag on the key invariant, such as overflow or a handshake in the wrong state.
Report: one line per finding with location, trigger and consequence; a list of what was skimmed.
“What do you skip?” Transistor-level sizing and style nits. Flag only what changes function, timing, or debuggability, and say the rest was skimmed.
“How do you report a finding?” One line per defect: location, trigger condition, and consequence. Then the fix and the check that would close it.
“The designer is in the room. How do you start?” Restate the block contract in one sentence and ask for correction. A wrong mental model reviewed confidently is worse than a slow review.
A weaker answer narrates the code top to bottom with no pass order. That tour treats a misplaced comment and a missing reset as equals.
Another weak answer lists suspicions with no trigger. Every finding needs the condition that exposes it, or it is not a finding.
Say so, name the riskiest state transition, and check that one path. An explicit gap beats silent hope.
Check the release timing and reset-controller/synchronizer intent even for one functional sink: one flop can violate recovery/removal. Multiple sinks add inconsistent-release risks, but fanout does not create or waive the timing requirement. Review domain-specific synchronous release, clocks available during release and STA/RDC evidence; see the canonical reset strategies lesson.
The review target is compiled and passed through Yosys, which reports the latch on in_ready (finding 4). The fixed block's bench runs with Icarus Verilog 13.0 (2026-10-17) and prints PASS: 200 + 200 + 100 counts to 500 without wrap, ready is low while busy, and a clear held for five cycles across the two-flop synchroniser returns the total to 0. The minute budgets are a suggested split, not a measurement.