Digital Design & Verification (Verilog, SystemVerilog, VLSI) Module 15 – Interview preparation for design and verification roles
RTL design interview problems, solved and tested
Solve RTL whiteboard problems the way interviews test them: a 50 % odd clock divider and a one-hot check, with short testbenches, synthesis counts and lint.
What you will learn
- Solve the odd clock divider and the one-hot check at a whiteboard, and explain the trade-offs of each
- Explain a design's flip-flops, longest path and first cycle after reset aloud, with synthesis numbers
- Verify a whiteboard design with a self-checking testbench of 20 to 30 lines, at more than one parameter value
- Avoid the coding slips interviewers listen for, and find them with lint before anyone else does
Before you start
What you’ll learn · Before you start
What you will learn
- Solve the odd clock divider and the one-hot check at a whiteboard, and explain the trade-offs of each
- Explain a design's flip-flops, longest path and first cycle after reset aloud, with synthesis numbers
- Verify a whiteboard design with a self-checking testbench of 20 to 30 lines, at more than one parameter value
- Avoid the coding slips interviewers listen for, and find them with lint before anyone else does
Before you start
On this page
A counter that wraps every three rising edges divides a clock by 3, but its output is high for one cycle in three, a 33 % duty cycle. Asked for 50 %, the answer needs one more flip-flop, on the falling edge, and a strong answer then says why that works, what it costs and how to prove it in the minutes that are left.
This lesson works two whiteboard problems in full, an odd clock divider and a one-hot check, in the order a strong answer takes: clarify, sketch, write, explain, verify. Each design ran on Icarus Verilog 13.0 and Verilator 5.052 under a testbench of 20 to 30 lines, Yosys 0.69 counted what each one costs, and Verilator’s lint caught four slips in a third file. The divider’s model runs in your browser.
A strong answer has five steps, and the RTL is only the third
An RTL whiteboard problem is a specification of one or two lines and a whiteboard or a shared editor to answer it on. The code is the part you have practised most, and it is one step of five. Step through an answer to the first problem of this lesson, divide a clock by 3 with a 50 % duty cycle: The share of each period that a clock or a pulse train is high: 50 % for a symmetric clock, 33.3 % for a divide-by-3 counter output that is high for one cycle in three. More in “Clocks for RTL designers: enables, gating, muxes”:
The drawing shows the whole scene; its 8 steps are listed below.
Every step as text
- The prompt. The prompt is one line: divide a clock by 3 with a 50 % duty cycle. Nothing is drawn or written yet, and the first minutes decide whether the rest answers the question that was asked.
- Clarify. Ask before you design: is the input clock itself 50 %? Must any odd N work, or only 3? Is the reset asynchronous? Is this for a chip or an FPGA? Each answer changes the design, and asking shows the interviewer how you work.
- Draw. Sketch the registers first: a 2-bit counter and pos_q on the rising edge, neg_q on the falling edge, and an OR gate in front of the output. The plan is checked here, while it costs nothing to change.
- Write. Write one always_ff block per clock edge, with the reset in each, and keep the output to the OR of two flip-flops. Name the signals as you drew them, so the interviewer can follow from the sketch.
- Explain. Say what synthesis builds: four flip-flops and three gates when N is 3, one flip-flop on the falling edge. Say what happens at reset: the counter starts at N - 1, so the first pulse is whole. The output is a clock, so timing analysis must be told it is one.
- Verify. Name the checks and what they catch: every period 30 ns and every high time 15 ns, for N of 3, 5 and 7, and one slip they would find, the counter reset to 0, which cuts the first pulse when N is 5.
- The follow-up. Then the follow-up: the input clock is high for only 40 % of its period. That is a new specification, so the answer goes back to the first step: the output is now high for 14 ns of every 30, a 46.7 % duty cycle.
- The whole answer. Five steps, the RTL only the third of them: four of the five happen around the code. Every problem of this lesson is answered in this order.
The order is the point. Questions before the sketch catch the cases the one-line prompt leaves out: the input clock’s own duty cycle, whether N is always 3, the kind of reset, a chip or an FPGA. The sketch shows the plan while it is still cheap to change. After the code come the two steps that show you understand what you wrote: what synthesis will build from it, with its flip-flops and its longest path, and how you would know it works. A follow-up question is a change of specification, so it sends you back to the first step instead of into a patch of the code.
Say the numbers, not the adjectives
“Four flip-flops and three gates, one flip-flop on the falling edge” tells an interviewer what you know; “small and fast” does not. Every design in this lesson comes with the numbers Yosys gave for it.
Thirteen problem families, and the trap in each
Here are thirteen families of RTL whiteboard problems. Each has one idea that solves it and one trap that a first answer falls into, and the lesson of this track that builds it:
| Family | The trap | Where this track builds it |
|---|---|---|
| Odd clock division, 50 % duty | Counting on one edge gives (N − 1) / 2 cycles high in N | This lesson, and clocking |
| Edge and pulse detectors | Detecting an edge of an input from another clock without synchronizing it first | Metastability |
| Sequence detectors | Forgetting that one match can overlap the next | The exercise below, and FSM coding |
| FIFOs and FIFO depth | Telling full from empty, and sizing for the burst, not the average | Synchronous FIFOs |
| Synchronizers | Synchronizing each bit of a bus on its own | Multi-bit crossings |
| Arbiters | Fixed priority that starves the last requester | Arbiters |
| Counters with load and enable | Which wins when load and enable come together | Counters and LFSRs |
| LFSRs | Taps that do not give the maximal length | Counters and LFSRs |
| Glitch-free clock switching | A multiplexer that cuts a clock pulse short | Clocking |
| Debouncers | Sampling the raw input with no synchronizer | Counters and LFSRs |
| Parity and CRC | Bit order, and the register’s starting value | DPI-C models |
| Rate matching | Losing words when the rates differ for a burst | Valid/ready |
| Pipelined arithmetic | Keeping the valid bit in step with the data | Pipelining |
This lesson solves the first family in full and adds a fourteenth problem that sits beside them, the one-hot check, because it is small enough to verify exhaustively and its three possible answers differ in cost in a way you can measure. Read the trap column as a list of follow-up questions: ask each of them yourself once your first answer works.
Checkpoint
Check: The five steps of an answer · Problem families and their traps
An odd divider takes its half cycle from the falling edge
The output must be high for N / 2 input cycles, 1.5 when N is 3, and rising edges alone can only measure whole cycles. So the design uses both edges. On each rising edge a counter steps through 0 to N − 1 and a flip-flop, pos_q, is set for the first (N − 1) / 2 counts. On each falling edge a second flip-flop, neg_q, copies pos_q half a cycle later. Their OR is high for (N − 1) / 2 cycles plus a half: N / 2. Step through one period of the divide-by-3, edge by edge, as the run below drew it:
The drawing shows the whole scene; its 7 steps are listed below.
Every step as text
- After reset. The reset has loaded count with 2, which is N - 1, and cleared both flip-flops, so clk_out is low. Starting at N - 1 means the first rising edge begins a whole period.
- 15 ns, rising edge. count steps from 2 to 0, and pos_q is loaded with whether that next count is below (N - 1) / 2 = 1: it is, so pos_q rises, and clk_out rises with it.
- 20 ns, falling edge. On the falling edge neg_q copies pos_q, half a cycle late. Both inputs of the OR are now 1.
- 25 ns, rising edge. count steps to 1, which is not below 1, so pos_q falls. neg_q still holds 1, so clk_out stays high: the OR changes only when the one input that moves is the only one that is 1.
- 30 ns, falling edge. neg_q copies the 0 and clk_out falls. It was high from 15 ns to 30 ns, 15 ns of a 30 ns period: 50 %.
- 35 ns, rising edge. count steps to 2 and pos_q stays 0. Nothing reaches the output; the falling edge at 40 ns copies the 0 again.
- 45 ns, rising edge. count wraps to 0 and pos_q rises: the next period starts 30 ns after the first. Each rising edge moved only the counter and pos_q, each falling edge only neg_q.
Two choices in div_odd.sv are worth saying aloud. pos_q is a flip-flop loaded from the next count, not a decode of the counter, because a decode of several changing bits can glitch: A short, unwanted pulse on the output of combinational logic while its inputs change at slightly different times. It is over before the next clock edge, so it harms only logic that uses the signal without that clock. More in “Counters, LFSRs, PWM and clock enables”, and this output is a clock. And the reset loads the counter with N − 1, not 0, so the first rising edge after reset starts a whole period. The OR gate itself is safe: each of its inputs changes on a different edge, and whenever one changes, the other holds still.
`timescale 1ns / 1ps
// Divides clk by an odd N with a 50 % duty cycle. On each rising edge a counter steps through 0 .. N-1 and pos_q is set
// for the first (N - 1) / 2 counts; on each falling edge neg_q copies pos_q; clk_out is their OR, high for N / 2 cycles.
`default_nettype none
module div_odd #(parameter int N = 3) (
input wire clk,
input wire rst_n, // asynchronous reset, active low
output logic clk_out
);
localparam int W = $clog2(N);
localparam int HALF = (N - 1) / 2;
logic [W-1:0] count, next_count;
logic pos_q, neg_q;
assign next_count = (count == W'(N - 1)) ? '0 : count + 1'b1;
always_ff @(posedge clk or negedge rst_n)
if (!rst_n) begin
`ifdef RESET_TO_ZERO
count <= '0; // the slip: for N of 5 or more the first pulse is cut short
`else
count <= W'(N - 1); // the last count, so the first edge starts a whole period
`endif
pos_q <= 1'b0;
end else begin
count <= next_count;
pos_q <= (next_count < W'(HALF)); // a flip-flop, so pos_q cannot glitch
end
always_ff @(negedge clk or negedge rst_n)
if (!rst_n) neg_q <= 1'b0;
else neg_q <= pos_q;
`ifdef RISING_ONLY
assign clk_out = pos_q; // the slip: high for (N - 1) / 2 cycles in N, 33 % when N is 3
`else
assign clk_out = pos_q | neg_q; // each input of the OR changes while the other holds still
`endif
endmodule
`default_nettype wire The model below runs the same rules in your browser, on a time grid of tenths of a period. Press Edit and set N to 5 or 7, or set DUTY to 40 to give the input clock a 40 % high time, and run it again:
"""The divide-by-N clock of div_odd.sv, for any odd N: change N or DUTY and run it again.
After reset the counter holds N - 1. On each rising edge it steps to the next count, 0 after N - 1, and pos is set
for the first (N - 1) / 2 counts; on each falling edge neg copies pos; the output is pos OR neg. Time moves in tenths
of an input period, so a duty cycle such as 40 % is exact.
"""
N = 3 # an odd divisor: 3, 5, 7 ...
DUTY = 50 # the input clock's high time, in percent of its period
STEPS = 10 # time steps in one input period
def simulate(n, duty, periods):
"""The input clock and the output, one value per time step."""
high_steps = duty * STEPS // 100
count, pos, neg = n - 1, 0, 0
clk, out = [], []
for t in range(n * periods * STEPS):
phase = t % STEPS
if phase == 0: # a rising edge
count = 0 if count == n - 1 else count + 1
pos = 1 if count < (n - 1) // 2 else 0
if phase == high_steps: # a falling edge
neg = pos
clk.append(1 if phase < high_steps else 0)
out.append(pos | neg)
return clk, out
clk, out = simulate(N, DUTY, 4)
rises = [t for t in range(1, len(out)) if out[t] and not out[t - 1]]
period = rises[1] - rises[0]
high = out[rises[0]:rises[1]].count(1)
shown = slice(rises[0], rises[0] + 2 * period) # two output periods, from a rising edge
draw = lambda wave: "".join("‾" if v else "_" for v in wave[shown])
print(f"N = {N}, input clock high for {DUTY} % of its period")
print("clk", draw(clk))
print("out", draw(out))
print(f"out: period {period / STEPS:g} input periods, high for {high / STEPS:g}, duty cycle {100 * high / period:.1f} %") Output
N = 3, input clock high for 50 % of its period clk ‾‾‾‾‾_____‾‾‾‾‾_____‾‾‾‾‾_____‾‾‾‾‾_____‾‾‾‾‾_____‾‾‾‾‾_____ out ‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾_______________‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾_______________ out: period 3 input periods, high for 1.5, duty cycle 50.0 %
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 odd_divider.py
Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Your run, in this browser
With N = 3 the output’s period is three input periods and it is high for 1.5 of them. Set DUTY to 40 and the high time becomes 1.4 periods, a 46.7 % duty cycle: the falling edge now comes at 40 % of the period, and neg_q follows it. That is the natural follow-up, and the answer is that this design passes on the input’s asymmetry; an exact 50 % output from an uneven input needs a clock-management block such as a PLL, or a faster clock to count with.
Key idea
An odd divider gets its half cycle from a flip-flop on the falling edge: pos_q is high for (N − 1) / 2 cycles, neg_q repeats it half a cycle later, and their OR is high for N / 2.
Checkpoint
Check: Odd division with a 50 % duty cycle
A 30-line testbench, run at three values of N, catches three slips
A testbench for a clock divider does not need waveforms: it can time the output. tb_div.sv records the time of each rising edge of clk_out, checks that each period is N × 10 ns and each high time N × 5 ns on a 10 ns clock, prints the first two failures and stops with $fatal if anything was wrong. N is a parameter of the testbench too, so one file tests every divisor. Here it runs at N = 3 and 7 on Icarus Verilog and at N = 5 on Verilator:
hdl/tb_div.sv
// Checks every period of clk_out (N × 10 ns) and every high time (N × 5 ns) for a 10 ns input clock.
`timescale 1ns / 1ps
module tb_div #(parameter int N = 3);
logic clk = 0, rst_n;
wire clk_out;
div_odd #(.N(N)) dut (.clk, .rst_n, .clk_out);
`ifdef INPUT_40
always begin clk = 1; #4; clk = 0; #6; end // the same 10 ns clock, high for 40 % of it
`else
always #5 clk = ~clk;
`endif
time rose = 0;
int periods = 0, wrong = 0;
task automatic fail(string what, time got);
wrong++;
if (wrong <= 2) $display("%s %0d ns at %0d ns", what, got, $time);
endtask
always @(posedge clk_out) begin
if (rose != 0 && $time - rose != 10 * N) fail("period", $time - rose);
if (rose != 0) periods++;
rose = $time;
end
always @(negedge clk_out) if (rose != 0 && 2 * ($time - rose) != 10 * N) fail("high", $time - rose);
initial begin
rst_n = 0;
#12 rst_n = 1;
#(200 * N) $display("N = %0d: %0d periods checked, %0d wrong: %s", N, periods, wrong, wrong != 0 ? "FAIL" : "PASS");
if (wrong != 0) $fatal(1, "clk_out is not a 50 %% divide-by-%0d", N);
$finish;
end
endmodule Commands · hdl/div_session.py
from session import run
# Divide by 3 and by 7 on Icarus Verilog, by 5 on Verilator, all with the same testbench.
run("""
iverilog -g2012 -o div3.vvp div_odd.sv tb_div.sv
vvp -n div3.vvp
iverilog -g2012 -Ptb_div.N=7 -o div7.vvp div_odd.sv tb_div.sv
vvp -n div7.vvp
verilator --binary -j 0 --quiet -GN=5 --top tb_div div_odd.sv tb_div.sv
obj_dir/Vtb_div +verilator+quiet
""") Output
# Icarus Verilog 13.0 (stable) · Verilator 5.052 $ iverilog -g2012 -o div3.vvp div_odd.sv tb_div.sv $ vvp -n div3.vvp N = 3: 19 periods checked, 0 wrong: PASS tb_div.sv:29: $finish called at 612000 (1ps) $ iverilog -g2012 -Ptb_div.N=7 -o div7.vvp div_odd.sv tb_div.sv $ vvp -n div7.vvp N = 7: 19 periods checked, 0 wrong: PASS tb_div.sv:29: $finish called at 1412000 (1ps) $ verilator --binary -j 0 --quiet -GN=5 --top tb_div div_odd.sv tb_div.sv $ obj_dir/Vtb_div +verilator+quiet N = 5: 19 periods checked, 0 wrong: PASS - tb_div.sv:29: Verilog $finish
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 div_session.py
All three pass with 19 periods checked. Running more than one value of N is a small parameter sweep: A regression that builds and tests a design at many values of its parameters, such as every depth and width of a FIFO, with each combination in a build of its own., and it matters here. The same testbench, given three slipped builds of the design through -D defines, fails each of them:
from session import run
# Three slips the same testbench catches: the rising edge alone, a counter reset to 0 (caught only when N is 5 or
# more), and an input clock that is high for 40 % of its period.
run("""
iverilog -g2012 -DRISING_ONLY -o rising.vvp div_odd.sv tb_div.sv
!vvp -n rising.vvp
iverilog -g2012 -DRESET_TO_ZERO -o zero3.vvp div_odd.sv tb_div.sv
vvp -n zero3.vvp
iverilog -g2012 -DRESET_TO_ZERO -Ptb_div.N=5 -o zero5.vvp div_odd.sv tb_div.sv
!vvp -n zero5.vvp
iverilog -g2012 -DINPUT_40 -o duty.vvp div_odd.sv tb_div.sv
!vvp -n duty.vvp
""") Output
# Icarus Verilog 13.0 (stable)
$ iverilog -g2012 -DRISING_ONLY -o rising.vvp div_odd.sv tb_div.sv
$ vvp -n rising.vvp
high 10 ns at 25 ns
high 10 ns at 55 ns
N = 3: 19 periods checked, 20 wrong: FAIL
FATAL: tb_div.sv:28: clk_out is not a 50 % divide-by-3
Time: 612000 Scope: tb_div
[exit status 1]
$ iverilog -g2012 -DRESET_TO_ZERO -o zero3.vvp div_odd.sv tb_div.sv
$ vvp -n zero3.vvp
N = 3: 19 periods checked, 0 wrong: PASS
tb_div.sv:29: $finish called at 612000 (1ps)
$ iverilog -g2012 -DRESET_TO_ZERO -Ptb_div.N=5 -o zero5.vvp div_odd.sv tb_div.sv
$ vvp -n zero5.vvp
high 15 ns at 30 ns
period 40 ns at 55 ns
N = 5: 20 periods checked, 2 wrong: FAIL
FATAL: tb_div.sv:28: clk_out is not a 50 % divide-by-5
Time: 1012000 Scope: tb_div
[exit status 1]
$ iverilog -g2012 -DINPUT_40 -o duty.vvp div_odd.sv tb_div.sv
$ vvp -n duty.vvp
high 14 ns at 34 ns
high 14 ns at 64 ns
N = 3: 19 periods checked, 20 wrong: FAIL
FATAL: tb_div.sv:28: clk_out is not a 50 % divide-by-3
Time: 612000 Scope: tb_div
[exit status 1]
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 slips_session.py
- The rising edge alone (RISING_ONLY) gives a high time of 10 ns in every period of 30: the 33 % answer.
- A counter reset to 0 (RESET_TO_ZERO) passes at N = 3 and fails at N = 5: the first pulse after reset is 15 ns instead of 25, because the first rising edge lands in the middle of a period. A testbench run only at N = 3 would have passed this bug.
- A 40 % input clock (INPUT_40) makes every high time 14 ns, as the model predicted. The design is right; the check reports that the specification’s assumption about the input no longer holds.
Testing one parameter value
Wrong: proving a parameterised design with its default value alone. The reset slip above is invisible at N = 3. Right: run the testbench at the default and at the next values up, here 5 and 7, which takes one more command each.
Checkpoint
Check: A short testbench at several parameter values
A one-hot check in one line, and three ways to build it
The second problem: given a bus x, say whether exactly one bit is set, as a one-hot: A code with one bit per choice and exactly one bit set, such as 0100 for choice 2 of four. A decoder makes it from a binary number with a shift; $onehot checks that a value is one. More in “Muxes, encoders and round-robin arbiters” state register or a grant vector must be. Subtracting 1 from x clears its lowest set bit and sets the bits below it, so x & (x − 1) is zero exactly when x has at most one bit set. That is SystemVerilog’s $onehot0; adding x ≠ 0 rules out the empty word and gives $onehot:
// Is exactly one bit of x set? x & (x - 1) clears the lowest set bit, so it is zero when x has at most one bit set;
// x != 0 then rules out the empty word.
`default_nettype none
module onehot_check #(parameter int W = 8) (
input wire [W-1:0] x,
output logic at_most_one, // zero or one bit set ($onehot0)
output logic exactly_one // one bit set ($onehot)
);
assign at_most_one = (x & (x - 1'b1)) == '0;
`ifdef NO_ZERO_TERM
assign exactly_one = at_most_one; // the slip: 0 counts as one-hot
`else
assign exactly_one = at_most_one && (x != '0);
`endif
endmodule
`default_nettype wire An 8-bit input has only 256 values, so the testbench tries them all, an exhaustive test: A test that applies every possible input value and checks each result, practical while the input has few bits: 256 values for an 8-bit word, 65,536 for a 16-bit one., against a reference that counts the ones in a loop: a different method, so a shared mistake is unlikely. The session also builds the version without the x ≠ 0 term, then checks the three forms of onehot_forms.sv, the trick, a count of the ones and a tree, against each other for all 65,536 values of 16 bits:
hdl/tb_onehot.sv
// Every 8-bit value against a reference that counts the ones in a loop.
module tb_onehot;
logic [7:0] x;
wire at_most_one, exactly_one;
onehot_check #(.W(8)) dut (.x, .at_most_one, .exactly_one);
int wrong = 0, ones;
initial begin
for (int v = 0; v < 256; v++) begin
x = 8'(v);
ones = 0;
for (int i = 0; i < 8; i++) ones += int'(x[i]);
#1;
if (exactly_one !== (ones == 1) || at_most_one !== (ones <= 1)) begin
wrong++;
$display("x = %b: exactly_one %b at_most_one %b, but %0d bits are set", x, exactly_one, at_most_one, ones);
end
end
$display("256 values checked, %0d wrong: %s", wrong, wrong != 0 ? "FAIL" : "PASS");
if (wrong != 0) $fatal(1, "the one-hot check is wrong");
$finish;
end
endmodule hdl/onehot_forms.sv
// Three ways to say "exactly one bit of x is set", for synthesis to compare.
`default_nettype none
module onehot_trick #(parameter int W = 8) (input wire [W-1:0] x, output logic y);
assign y = ((x & (x - 1'b1)) == '0) && (x != '0);
endmodule
module onehot_count #(parameter int W = 8) (input wire [W-1:0] x, output logic y);
always_comb begin
int ones;
ones = 0;
for (int i = 0; i < W; i++) ones += int'(x[i]);
y = (ones == 1);
end
endmodule
// A tree of pairs: each node knows whether any bit below it is set, and whether more than one is.
module onehot_tree #(parameter int W = 8) (input wire [W-1:0] x, output logic y);
logic [2*W-2:0] any, many; // heap order: node k has children 2k+1 and 2k+2; the leaves are W-1 .. 2W-2
always_comb begin
for (int i = 0; i < W; i++) begin
any[W-1+i] = x[i];
many[W-1+i] = 1'b0;
end
for (int k = W-2; k >= 0; k--) begin
any[k] = any[2*k+1] | any[2*k+2];
many[k] = many[2*k+1] | many[2*k+2] | (any[2*k+1] & any[2*k+2]);
end
y = any[0] & ~many[0];
end
endmodule
`default_nettype wire hdl/tb_forms.sv
// The three one-hot checks of onehot_forms.sv at 16 bits, against a loop that counts the ones, for all 65,536 values.
module tb_forms;
logic [15:0] x;
wire trick, count, tree;
onehot_trick #(.W(16)) u_trick (.x, .y(trick));
onehot_count #(.W(16)) u_count (.x, .y(count));
onehot_tree #(.W(16)) u_tree (.x, .y(tree));
int wrong = 0, hits = 0, ones;
initial begin
for (int v = 0; v < 65536; v++) begin
x = 16'(v);
ones = 0;
for (int i = 0; i < 16; i++) ones += int'(x[i]);
#1;
if (ones == 1) hits++;
if (trick !== (ones == 1) || count !== (ones == 1) || tree !== (ones == 1)) wrong++;
end
$display("65536 values, %0d of them one-hot: %0d wrong: %s", hits, wrong, wrong != 0 ? "FAIL" : "PASS");
if (wrong != 0) $fatal(1, "the forms disagree");
$finish;
end
endmodule Commands · hdl/onehot_session.py
from session import run
# The whiteboard answer for all 256 values on both simulators, the version without the x != 0 term, then the three
# forms of onehot_forms.sv for all 65,536 values of 16 bits.
run("""
iverilog -g2012 -o onehot.vvp onehot_check.sv tb_onehot.sv
vvp -n onehot.vvp
verilator --binary -j 0 --quiet --top tb_onehot onehot_check.sv tb_onehot.sv
obj_dir/Vtb_onehot +verilator+quiet
iverilog -g2012 -DNO_ZERO_TERM -o slip.vvp onehot_check.sv tb_onehot.sv
!vvp -n slip.vvp
verilator --binary -j 0 --quiet --top tb_forms -Mdir obj_forms onehot_forms.sv tb_forms.sv
obj_forms/Vtb_forms +verilator+quiet
""") Output
# Icarus Verilog 13.0 (stable) · Verilator 5.052
$ iverilog -g2012 -o onehot.vvp onehot_check.sv tb_onehot.sv
$ vvp -n onehot.vvp
256 values checked, 0 wrong: PASS
tb_onehot.sv:20: $finish called at 256 (1s)
$ verilator --binary -j 0 --quiet --top tb_onehot onehot_check.sv tb_onehot.sv
$ obj_dir/Vtb_onehot +verilator+quiet
256 values checked, 0 wrong: PASS
- tb_onehot.sv:20: Verilog $finish
$ iverilog -g2012 -DNO_ZERO_TERM -o slip.vvp onehot_check.sv tb_onehot.sv
$ vvp -n slip.vvp
x = 00000000: exactly_one 1 at_most_one 1, but 0 bits are set
256 values checked, 1 wrong: FAIL
FATAL: tb_onehot.sv:19: the one-hot check is wrong
Time: 256 Scope: tb_onehot
[exit status 1]
$ verilator --binary -j 0 --quiet --top tb_forms -Mdir obj_forms onehot_forms.sv tb_forms.sv
$ obj_forms/Vtb_forms +verilator+quiet
65536 values, 16 of them one-hot: 0 wrong: PASS
- tb_forms.sv:20: Verilog $finish
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 onehot_session.py
The slip fails on exactly one value of 256, x = 0, which a handful of hand-picked tests could easily miss and an exhaustive one cannot. The three forms agree on all 65,536 values of 16 bits, 16 of which are one-hot. Equal outputs are not equal hardware, though, and that is what the next section measures.
Programmer Calculator (Bitwise) Work x & (x − 1) by hand on a few words, such as 0b0100 and 0b0110, and watch the lowest set bit clear.Checkpoint
Check: The one-hot check and its exhaustive test
Explain timing, area and reset aloud, with the numbers synthesis gives
After the code comes its cost, and Yosys answers in seconds: stat counts the cells and ltp -noff
reports the longest path through gates between flip-flops and ports, the design’s logic depth: The number of gates on the longest path between flip-flops or ports. Each gate adds its delay, so a deeper path needs a slower clock; Yosys's ltp command reports it.. The session below synthesises the divider at N = 3 and 7, then each one-hot form at 8, 32 and 64 bits:
from session import run
# What synthesis builds: the divider for N = 3 and 7, then the three one-hot checks at 8, 32 and 64 bits, each with
# its cells and its longest path through gates (Yosys's ltp -noff).
run("""
yosys -q -p "read_verilog -sv div_odd.sv; synth -top div_odd; tee -q -o div3.txt stat"
yosys -q -p "read_verilog -sv div_odd.sv; chparam -set N 7 div_odd; synth -top div_odd; tee -q -o div7.txt stat"
grep -H -E "cells|DFF" div3.txt div7.txt
for f in trick count tree; do for w in 8 32 64; do yosys -q -p "read_verilog -sv onehot_forms.sv; chparam -set W $w onehot_$f; synth -top onehot_$f; tee -q -o $f$w.txt stat; tee -q -a $f$w.txt ltp -noff"; done; done
python3 summary.py trick8.txt trick32.txt trick64.txt count8.txt count32.txt count64.txt tree8.txt tree32.txt tree64.txt
""") Output
# Yosys 0.69 $ yosys -q -p "read_verilog -sv div_odd.sv; synth -top div_odd; tee -q -o div3.txt stat" $ yosys -q -p "read_verilog -sv div_odd.sv; chparam -set N 7 div_odd; synth -top div_odd; tee -q -o div7.txt stat" $ grep -H -E "cells|DFF" div3.txt div7.txt div3.txt: 7 cells div3.txt: 1 $_DFF_NN0_ div3.txt: 2 $_DFF_PN0_ div3.txt: 1 $_DFF_PN1_ div7.txt: 14 cells div7.txt: 1 $_DFF_NN0_ div7.txt: 2 $_DFF_PN0_ div7.txt: 2 $_DFF_PN1_ $ for f in trick count tree; do for w in 8 32 64; do yosys -q -p "read_verilog -sv onehot_forms.sv; chparam -set W $w onehot_$f; synth -top onehot_$f; tee -q -o $f$w.txt stat; tee -q -a $f$w.txt ltp -noff"; done; done $ python3 summary.py trick8.txt trick32.txt trick64.txt count8.txt count32.txt count64.txt tree8.txt tree32.txt tree64.txt onehot_trick 8 bits: 19 cells, longest path 8 gates onehot_trick 32 bits: 92 cells, longest path 32 gates onehot_trick 64 bits: 188 cells, longest path 64 gates onehot_count 8 bits: 18 cells, longest path 6 gates onehot_count 32 bits: 112 cells, longest path 17 gates onehot_count 64 bits: 273 cells, longest path 21 gates onehot_tree 8 bits: 19 cells, longest path 5 gates onehot_tree 32 bits: 91 cells, longest path 8 gates onehot_tree 64 bits: 187 cells, longest path 9 gates
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 area_session.py
The divider is 7 cells at N = 3: four flip-flops (two counter bits, pos_q and neg_q; one counter bit resets to 1, because N − 1 is 2) and three gates. At N = 7 it is 14 cells with five flip-flops: one more counter bit, and the counter’s logic. The one-hot forms tell the more interesting story:
| Form | Cells at 64 bits; longest path at 8, 32, 64 bits | What to say aloud |
|---|---|---|
| x & (x − 1) | 188 cells; 8, 32 and 64 gates | A borrow ripples through every bit: depth grows with the width |
| Count the ones | 273 cells; 6, 17 and 21 gates | An adder tree: shallow, but the largest |
| A tree of pairs | 187 cells; 5, 8 and 9 gates | Each node keeps “any bit set” and “two or more”: depth grows with log₂ of the width |
At 64 bits the trick and the tree cost the same, 188 and 187 cells, but the trick’s longest path is 64 gates against the tree’s 9, so only the tree would meet a fast clock. That is the answer to the follow-up “your check is on a 64-bit bus at 1 GHz”: keep x & (x − 1) for narrow buses and for its one line of code, and know the tree. Say the reset behaviour as plainly: the divider’s output is low during reset, and its first period after reset is whole, which the N = 5 run proved.
Key idea
Explain a design with three numbers from synthesis: its flip-flops, its cells and its longest path. The one-hot trick and the tree both took about 187 cells at 64 bits, and their paths were 64 and 9 gates.
Checkpoint
Check: Flip-flops, cells and logic depth, said aloud
Four coding slips that lint finds before an interviewer does
Some slips simulate without complaint and build the wrong hardware, so they are the first things to look for in your
own whiteboard RTL. edge_count.sv has four of them: a blocking assignment in a clocked block, eight bits assigned
into four, an if without a final else in a combinational block, and a case with no default. Verilator’s lint with
every warning on, --lint-only -Wall, names all four:
hdl/edge_count.sv
// A rising-edge counter with four slips that interviewers listen for; -DFIXED builds the corrected lines.
`default_nettype none
module edge_count (
input wire clk,
input wire rst_n,
input wire din, // already synchronous to clk
input wire load,
input wire [7:0] len, // a field of a register: only its low 4 bits are a count
input wire [1:0] mode,
output logic rise,
output logic [3:0] count,
output logic busy,
output logic led
);
logic din_q;
`ifndef FIXED
always_ff @(posedge clk) din_q = din; // slip 1: a blocking assignment in a clocked block
always_ff @(posedge clk or negedge rst_n)
if (!rst_n) count <= '0;
else if (load) count <= len; // slip 2: 8 bits into 4, without a word
else if (rise) count <= count + 4'd1;
always_comb
if (mode == 2'd1) busy = 1'b1; // slip 3: no final else, so busy must remember: a latch
else if (mode == 2'd2) busy = 1'b0;
always_comb
case (mode) // slip 4: no default, so modes 1 and 2 leave led alone
2'd0: led = 1'b0;
2'd3: led = 1'b1;
endcase
`else
always_ff @(posedge clk or negedge rst_n)
if (!rst_n) din_q <= 1'b0;
else din_q <= din;
always_ff @(posedge clk or negedge rst_n)
if (!rst_n) count <= '0;
else if (load) count <= len[3:0];
else if (rise) count <= count + 4'd1;
wire unused_len = |len[7:4]; // the other field of the register, read elsewhere
always_comb busy = (mode == 2'd1);
always_comb
case (mode)
2'd3: led = 1'b1;
default: led = 1'b0;
endcase
`endif
assign rise = din & ~din_q;
endmodule
`default_nettype wire Commands · hdl/lint_session.py
from session import run
# Verilator's lint with every warning on, for the edge counter as written and with -DFIXED; the first run's messages
# go to lint.txt, and grep keeps the first line of each.
run("""
!verilator --lint-only -Wall edge_count.sv > lint.txt 2>&1
grep "^%" lint.txt
verilator --lint-only -Wall --quiet -DFIXED edge_count.sv
""") Output
# Verilator 5.052 $ verilator --lint-only -Wall edge_count.sv > lint.txt 2>&1 [exit status 1] $ grep "^%" lint.txt %Warning-WIDTHTRUNC: edge_count.sv:20:26: Operator ASSIGNDLY expects 4 bits on the Assign RHS, but Assign RHS's VARREF 'len' generates 8 bits. %Warning-BLKSEQ: edge_count.sv:17:34: Blocking assignment '=' in sequential logic process %Warning-UNUSEDSIGNAL: edge_count.sv:8:22: Bits of signal are not used: 'len'[7:4] %Warning-CASEINCOMPLETE: edge_count.sv:26:5: Case values incompletely covered (example pattern 0x1) %Warning-LATCH: edge_count.sv:22:3: Latch inferred for signal 'busy' (not all control paths of combinational always assign a value) %Warning-LATCH: edge_count.sv:25:3: Latch inferred for signal 'led' (not all control paths of combinational always assign a value) %Error: Exiting due to 6 warning(s) $ verilator --lint-only -Wall --quiet -DFIXED edge_count.sv
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 lint_session.py
Six warnings for four slips: the width slip also leaves the field’s upper bits unread (UNUSEDSIGNAL), and the case
without a default is reported twice, as CASEINCOMPLETE and as a latch: A storage element that passes its input while its enable is on and holds its last value while the enable is off. A combinational block with a path that assigns nothing describes one. More in “Combinational logic with assign and always_comb”. The fixed build
assigns len[3:0], writes busy as one expression with no path left out, gives the case a default and uses <= in
the clocked block. It lints with no warning at all. The upper bits of len belong to another field of the register, so
the fix reads them into a wire named unused_len: Verilator’s documentation says the name “unused” is recognised and
suppresses the warning, which keeps the reason visible in the code instead of in a switch on the command line.
A latch from a missing else
Wrong: if (mode == 1) busy = 1; else if (mode == 2) busy = 0; in an always_comb, which leaves busy unassigned for
modes 0 and 3, so it must remember: a latch. Right: give every path a value, here busy = (mode == 1);, or start
the block with a default assignment.
Checkpoint
Check: Coding slips that lint finds
Practise: an overlapping sequence detector
The exercise is a third family from the table: a sequence detector: A state machine that reads a serial input one bit per clock cycle and raises its output when the latest bits match a pattern. An overlapping detector lets the last bits of one match start the next. for the pattern 1101 on a serial input, modelled in Python as the RTL would build it, a state that counts the bits matched so far and a table of next states. The starter has two classic slips in its table; the sample tests include an overlap, a run of ones and a pattern broken one bit early.
Exercise · Medium · Python
An overlapping 1101 detector, cycle by cycle
Model a sequence detector the way the RTL would build it: a Mealy machine that reads one bit per clock cycle and outputs 1 in the cycle whose bit completes the pattern 1101. Matches may overlap, so the last 1 of one match can be the first 1 of the next: 1101101 holds two matches, ending at positions 3 and 6.
Write detect(bits), which takes the bits as a string of 0 and 1 characters, oldest first, and returns a string of the same length: 1 where a match ends, 0 everywhere else. The starter keeps a state, the number of bits of 1101 that the latest bits match, in a table of next states, and reads the table once per bit. Its table has two classic slips: after a match it starts again from nothing, and a third 1 in a row throws away the two before it.
The sample tests include the overlap, a run of ones, a pattern broken one bit before its end, and the empty input.
Starter code · detector.py
# The state is how many bits of 1101 the latest bits match: 0, 1 ("1"), 2 ("11") or 3 ("110").
NEXT = {
(0, "0"): 0, (0, "1"): 1,
(1, "0"): 0, (1, "1"): 2,
(2, "0"): 3, (2, "1"): 0,
(3, "0"): 0, (3, "1"): 0,
}
def detect(bits):
"""1 in each cycle whose bit completes 1101, overlaps allowed; 0 in every other cycle."""
state, out = 0, []
for b in bits:
out.append("1" if state == 3 and b == "1" else "0")
state = NEXT[(state, b)]
return "".join(out) The sample tests · test_detector.py
from detector import detect
def test_one_match():
"""the pattern alone: the output rises in the cycle of its last bit"""
assert detect("1101") == "0001"
def test_overlapping_matches():
"""the last 1 of a match starts the next one"""
assert detect("1101101") == "0001001"
def test_a_run_of_ones():
"""111 still ends in 11, so 11101 is a match"""
assert detect("11101") == "00001"
def test_broken_one_bit_early():
"""1100 breaks the pattern; the detector starts again and finds the match that follows"""
assert detect("110011101") == "000000001"
def test_no_match_and_no_input():
"""0101 is not 1101, 110 is not finished, and an empty input gives an empty output"""
assert detect("0101") == "0000"
assert detect("110") == "000"
assert detect("") == "" A hint
After a match the latest bits are 1101. The longest ending of 1101 that is also a beginning of 1101 is 1, so the next state after a match is 1, not 0. Check the other seven entries the same way: for state 2 (11) and a 1, the latest bits are 111, whose longest such ending is 11.
Results of the sample tests
| Test | Result | Details |
|---|
What your code printed
The sample tests run on this device, in your browser (Pyodide): nothing is sent to mysmartcopilot.com. The first run downloads Python (about 13.5 MB), which is kept for the next runs. A check in your browser is feedback for you, not proof that the code is right for every input.
Then check yourself on the seven ideas of this lesson, from the five steps to lint, in a session that leads with your weakest one:
Practice that adapts to you
Practise this lesson
14 questions about 7 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.
Interview questions
Warm-up (fresher): why does an odd clock divider with a 50 % duty cycle need the falling edge of the clock?
Because the output must be high for N / 2 input periods, and N / 2 is not a whole number when N is odd: 1.5 periods for a divide-by-3. Logic clocked on rising edges alone changes only at whole periods, so it can give 1 or 2 periods high in 3, a 33 % or 67 % duty cycle, never 50 %. The fix keeps the counter and a flip-flop pos_q on the rising edge, pos_q high for (N − 1) / 2 periods, and adds a flip-flop on the falling edge that copies pos_q half a period later. Their OR is high for (N − 1) / 2 periods plus a half. The run measured 15 ns high in every 30 ns period on a 10 ns clock, and 14 ns when the input clock was high for only 40 %.
Warm-up (fresher): what does x & (x − 1) compute, and how does it test a word for one-hot?
Subtracting 1 from x clears its lowest set bit and sets every bit below it, so ANDing the result with x clears exactly
the lowest set bit of x. The result is zero when x had at most one bit set, so (x & (x - 1)) == 0 is a check for
zero-or-one-hot, SystemVerilog’s $onehot0, and adding x != 0 makes it exactly one-hot, $onehot. The exhaustive test
of this lesson showed why the second term matters: without it, the check failed on one value of 256, the word 0.
Two follow-ups can come next. Choose the answer you would give; the feedback says what each one gets right or leaves out:
Checkpoint
Interview round: two follow-up questions
Two harder questions follow, one about a divider whose first pulse is short on the board and one about a one-hot check that fails timing on a wide bus. Sketch your opening move for each before you read the first step under it:
Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.
-
Fresher to mid
Your divide-by-5 clock with a 50 % duty cycle passed its testbench at N = 3, and on the board every period is right except the first output pulse after reset, which is short. What went wrong, and what do you change in the design and in the testbench?
-
Mid to senior
A grant checker computes (g & (g - 1)) == 0 on a 64-bit grant vector, and the path fails timing at 1 GHz. Explain why, propose a structure that meets timing, and say how you would prove the new one equal to the old.
Flashcards · 8 cards
Recall it, then check
Say the answer to yourself before you look: recalling it is what makes it stick.
-
What are the five steps of a strong RTL whiteboard answer?
Clarify, sketch, write, explain and verify. A follow-up question is a change of specification, which sends you back to the first step.
-
Why does a divide-by-3 with a 50 % duty cycle need the falling edge?
The output must be high for 1.5 input periods, and rising-edge logic changes only at whole periods, giving 33 % or 67 %. A flip-flop on the falling edge repeats pos_q half a period later, and their OR is high for N / 2.
-
Why does the odd divider's reset load its counter with N - 1 instead of 0?
Loading N - 1 makes the counter wrap at the first rising edge, so the first period after reset is a whole one. Reset to 0, the first pulse after reset was 15 ns instead of 25 at N = 5, a slip invisible at N = 3.
-
What happens to the odd divider's output when the input clock is high for only 40 %?
Its high time follows the input's falling edge: 1.4 periods in 3, 14 ns of 30, a 46.7 % duty cycle. An exact 50 % from an uneven input needs a PLL or a faster clock to count with.
-
Why run a parameterised design's testbench at more than its default value?
Some slips show only at other values: the counter reset to 0 passed at N = 3 and failed at N = 5.
-
How does (x & (x - 1)) == 0 test a word?
Subtracting 1 clears x's lowest set bit and sets the bits below it, so the AND clears exactly that bit; zero means at most one bit is set, $onehot0. Adding x != 0 gives exactly one-hot, $onehot.
-
Why does a wide, fast bus need a tree for its one-hot check?
x & (x - 1) ripples a borrow through every bit, a 64-gate path at 64 bits, while a tree of pairs that keeps "any bit set" and "two or more" had a 9-gate path at about the same 187 cells.
-
Which Yosys commands give the numbers to say aloud about a design?
stat counts the cells and flip-flops, and ltp -noff reports the longest path through gates between flip-flops and ports, the logic depth.
Key takeaways
- A whiteboard answer runs in five steps, clarify, sketch, write, explain and verify, and a follow-up is a new specification that sends you back to the first.
- Each problem family has one trap that a first answer falls into, and the trap is the follow-up to ask yourself.
- An odd divider gets its half cycle from a flip-flop on the falling edge, so its output is high for N / 2 periods; with a 40 % input clock it was high for 14 ns of 30.
- One self-checking testbench run at N = 3, 5 and 7 caught three slips, including a reset to 0 that only N = 5 shows.
- x & (x − 1) checks for at most one set bit in one line, but its path was 64 gates deep at 64 bits against 9 for a tree of the same size, so a wide, fast bus needs the tree.
- Verilator’s
--lint-only -Wallreported six warnings for four slips in edge_count.sv and none for the fixed build.
Terms in this lesson
- exhaustive test
- A test that applies every possible input value and checks each result, practical while the input has few bits: 256 values for an 8-bit word, 65,536 for a 16-bit one.
- logic depth
- The number of gates on the longest path between flip-flops or ports. Each gate adds its delay, so a deeper path needs a slower clock; Yosys's ltp command reports it.
- sequence detector
- A state machine that reads a serial input one bit per clock cycle and raises its output when the latest bits match a pattern. An overlapping detector lets the last bits of one match start the next.
References
- IEEE 1800-2023: IEEE Standard for SystemVerilog, Unified Hardware Design, Specification, and Verification Language (IEEE Standards Association)
- Icarus Verilog documentation: iverilog command line flags (-g2012, -D, -P) (Icarus Verilog)
- Verilator documentation: verilator arguments (--binary, -G, --lint-only, -Wall) (Verilator)
- Verilator documentation: warnings (BLKSEQ, CASEINCOMPLETE, LATCH, UNUSEDSIGNAL, WIDTHTRUNC) (Verilator)
- Yosys command reference: status commands (stat, ltp) (YosysHQ)
- Yosys command reference: generic synthesis (synth) (YosysHQ)
Related tools
Report a problem with this lesson
Kept only in this browser. Your Learn progress