Working Verilog & SystemVerilog, explained line by line
16 examples of the circuits and testbench patterns that come up again and again in RTL and verification interviews. Each one shows the code, why it's written that way, and how interviewers extend it. The 14 Verilog examples were simulated and self-checked before being published.
16EXAMPLES
14SIMULATED & SELF-CHECKED
CopyEVERY BLOCK IS SELECTABLE
Tested, not just typed. Every example marked “Simulated” was compiled with Icarus Verilog and run against a self-checking testbench before publishing.
Read it, then rewrite it. Copy is fine for study, but in interviews you'll write these from memory. Close the page and try.
Looking for something else?Tutorials teach step by step, guides go deep on one topic, and the RTL questions test you.
BasicsBeginner✓ Simulated & self-checked
Flip-flops: asynchronous vs synchronous reset
The same D flip-flop written two ways, so you can see exactly how the reset styles differ.
dff.v
// Two D flip-flops side by side: asynchronous vs synchronous active-low reset
module dff_resets (
input wire clk,
input wire rst_n,
input wire d,
output reg q_async, // clears immediately when rst_n falls
output reg q_sync // clears on the next rising clock edge
);
always @(posedge clk or negedge rst_n)
if (!rst_n) q_async <= 1'b0;
else q_async <= d;
always @(posedge clk)
if (!rst_n) q_sync <= 1'b0;
else q_sync <= d;
endmodule
WHY IT'S WRITTEN THIS WAY
q_async has negedge rst_n in its sensitivity list, so it clears the instant reset falls, even with no clock.
q_sync only looks at rst_n on a clock edge, so reset behaves like any other data input and is timed by STA.
Non-blocking <= in both: every clocked block should use it.
Interview angle: Be ready to say why the release of an asynchronous reset is the risky part, and how a reset synchronizer fixes it (see below).
BasicsBeginner✓ Simulated & self-checked
Parameterized up/down counter with load and enable
A reusable counter: set the width once, load any value, count in either direction.
counter.v
// Parameterized up/down counter with synchronous load and enable
module updown_counter #(parameter W = 8) (
input wire clk,
input wire rst_n,
input wire en,
input wire load,
input wire up, // 1 = count up, 0 = count down
input wire [W-1:0] load_val,
output reg [W-1:0] count
);
always @(posedge clk or negedge rst_n)
if (!rst_n) count <= {W{1'b0}};
else if (load) count <= load_val; // load has priority over counting
else if (en) count <= up ? count + 1'b1 : count - 1'b1;
endmodule
WHY IT'S WRITTEN THIS WAY
parameter W makes one module serve every width. Instantiate with #(.W(16)).
The if / else if order sets priority: reset, then load, then count. Say so explicitly in interviews.
Wrap-around is automatic: an 8-bit counter at 255 counting up goes to 0. The testbench checks 250 + 7 → 1.
Interview angle: Interviewers often extend this live: "now make it stop at a limit" or "add a terminal-count output".
BasicsIntermediate✓ Simulated & self-checked
8-bit maximal-length LFSR
A linear-feedback shift register that cycles through all 255 non-zero states: a cheap pseudo-random source for BIST, scramblers and test patterns.
lfsr.v
// 8-bit maximal-length Fibonacci LFSR: x^8 + x^6 + x^5 + x^4 + 1 (period 255)
module lfsr8 (
input wire clk,
input wire rst_n,
input wire en,
output reg [7:0] q
);
wire fb = q[7] ^ q[5] ^ q[4] ^ q[3]; // taps 8,6,5,4 -> bit indices 7,5,4,3
always @(posedge clk or negedge rst_n)
if (!rst_n) q <= 8'h01; // any non-zero seed; all-zeros is a lock-up state
else if (en) q <= {q[6:0], fb};
endmodule
WHY IT'S WRITTEN THIS WAY
Taps 8, 6, 5, 4 come from a primitive polynomial, which is what makes the period the maximum 2⁸ − 1 = 255. Our testbench counts it.
The seed must be non-zero. All zeros is a lock-up state, because XOR of zeros is zero forever.
It uses only flip-flops and a few XOR gates, which is why LFSRs appear in DFT (LBIST pattern generators) and serial links.
Interview angle: Expect "why can't the state be all zeros?" and "how would you get a 256-state sequence?"
BasicsBeginner✓ Simulated & self-checked
Rising and falling edge detector
Turns a level change into a one-clock-cycle pulse. It's used everywhere, from button presses to start signals.
edge.v
// Rising- and falling-edge detector: one-cycle pulses, input must already be synchronous
module edge_detect (
input wire clk,
input wire rst_n,
input wire sig,
output wire rise,
output wire fall
);
reg sig_d;
always @(posedge clk or negedge rst_n)
if (!rst_n) sig_d <= 1'b0;
else sig_d <= sig;
assign rise = sig & ~sig_d;
assign fall = ~sig & sig_d;
endmodule
WHY IT'S WRITTEN THIS WAY
The trick is comparing the signal with its own value one cycle ago (sig_d).
The outputs are combinational (assign), so the pulse appears in the same cycle the change is seen.
The input must already be synchronous to clk. For an asynchronous input, add a 2-flop synchronizer first.
Interview angle: A common follow-up: "what happens if sig comes from another clock domain?"
ClockingIntermediate✓ Simulated & self-checked
Clock-enable (tick) generator instead of a divided clock
Produces a one-cycle tick every N clocks, so slow logic can run on the main clock using an enable.
tick.v
// Clock-enable generator: one-cycle 'tick' every N clocks (use instead of a divided clock)
module tick_gen #(parameter N = 10) (
input wire clk,
input wire rst_n,
output reg tick
);
localparam CW = (N > 1) ? $clog2(N) : 1;
reg [CW-1:0] cnt;
always @(posedge clk or negedge rst_n)
if (!rst_n) begin
cnt <= {CW{1'b0}};
tick <= 1'b0;
end else if (cnt == N-1) begin
cnt <= {CW{1'b0}};
tick <= 1'b1;
end else begin
cnt <= cnt + 1'b1;
tick <= 1'b0;
end
endmodule
WHY IT'S WRITTEN THIS WAY
Dividing a clock with a counter bit creates a new clock that CTS and STA must handle, and it can cause skew between domains. An enable keeps everything in one clock domain.
The counter width is computed with $clog2(N), so it adapts to any N.
Downstream logic uses if (tick) ... inside its normal always @(posedge clk) block.
Interview angle: This is the answer interviewers want when they ask "how do you run part of a design at 1/10 the speed?"
ClockingIntermediate✓ Simulated & self-checked
Reset synchronizer
Asynchronous assertion, synchronous de-assertion: the standard way to deliver reset into each clock domain.
rstsync.v
// Reset synchronizer: asynchronous assertion, synchronous de-assertion
module reset_sync (
input wire clk,
input wire arst_n, // raw asynchronous reset, active low
output wire rst_n // safe to use in the clk domain
);
reg r1, r2;
always @(posedge clk or negedge arst_n)
if (!arst_n) {r2, r1} <= 2'b00; // assert immediately, no clock needed
else {r2, r1} <= {r1, 1'b1}; // release after two clean edges
assign rst_n = r2;
endmodule
WHY IT'S WRITTEN THIS WAY
Reset asserts immediately through the asynchronous clear, even if the clock is stopped.
Reset releases only after two clean clock edges, so every flop in the domain leaves reset in the same cycle, away from the timing window.
Use one synchronizer per clock domain. Never share one synchronized reset across unrelated clocks.
Interview angle: Pairs with the flip-flop example above. Interviewers love the "why not just use the raw reset?" follow-up.
InterfacesIntermediate✓ Simulated & self-checked
Push-button debouncer
Mechanical buttons bounce for a few milliseconds. This module ignores the bounces and outputs one clean transition.
debounce.v
// Push-button debouncer: output changes only after the input is stable for STABLE clocks
module debounce #(parameter STABLE = 16) (
input wire clk,
input wire rst_n,
input wire btn_raw, // asynchronous, bouncy
output reg btn_clean
);
// 1) synchronize the raw input (it is asynchronous to clk)
reg s1, s2;
always @(posedge clk or negedge rst_n)
if (!rst_n) {s2, s1} <= 2'b00;
else {s2, s1} <= {s1, btn_raw};
// 2) count how long the synchronized input has differed from the output
reg [$clog2(STABLE+1)-1:0] cnt;
always @(posedge clk or negedge rst_n)
if (!rst_n) begin
cnt <= 0; btn_clean <= 1'b0;
end else if (s2 == btn_clean) begin
cnt <= 0; // agrees with output: nothing to do
end else if (cnt == STABLE-1) begin
btn_clean <= s2; // stable long enough: accept the new level
cnt <= 0;
end else begin
cnt <= cnt + 1'b1;
end
endmodule
WHY IT'S WRITTEN THIS WAY
Step 1 synchronizes the raw button, which is asynchronous to the clock, through two flops.
Step 2 only accepts a new level after it has been stable for STABLE clocks. Choose STABLE for about 5–20 ms at your clock rate.
The testbench toggles the input six times rapidly (ignored), then holds it (accepted).
Interview angle: It shows you think about the real world: asynchronous inputs, metastability, and noise.
MemoriesIntermediate✓ Simulated & self-checked
Synchronous FIFO with correct full/empty flags
The single most-asked RTL design question. This version's full flag stays correct after the pointers wrap around.
fifo.v
// Synchronous FIFO: DEPTH must be a power of two. Pointers carry one extra wrap bit.
module sync_fifo #(parameter WIDTH = 8, parameter DEPTH = 8) (
input wire clk,
input wire rst_n,
input wire wr_en,
input wire [WIDTH-1:0] din,
input wire rd_en,
output reg [WIDTH-1:0] dout,
output wire full,
output wire empty
);
localparam AW = $clog2(DEPTH);
reg [WIDTH-1:0] mem [0:DEPTH-1];
reg [AW:0] wr_ptr, rd_ptr; // AW+1 bits: the MSB counts wraps
assign empty = (wr_ptr == rd_ptr);
assign full = (wr_ptr[AW] != rd_ptr[AW]) && // wrapped a different number of times
(wr_ptr[AW-1:0] == rd_ptr[AW-1:0]);
wire do_wr = wr_en && !full;
wire do_rd = rd_en && !empty;
always @(posedge clk) if (do_wr) mem[wr_ptr[AW-1:0]] <= din;
always @(posedge clk or negedge rst_n)
if (!rst_n) begin
wr_ptr <= 0; rd_ptr <= 0; dout <= {WIDTH{1'b0}};
end else begin
if (do_wr) wr_ptr <= wr_ptr + 1'b1;
if (do_rd) begin
dout <= mem[rd_ptr[AW-1:0]];
rd_ptr <= rd_ptr + 1'b1;
end
end
endmodule
WHY IT'S WRITTEN THIS WAY
Each pointer has one extra MSB (the wrap bit). Empty means the pointers are equal. Full means the address bits are equal but the wrap bits differ.
Writes are blocked when full and reads are blocked when empty (do_wr and do_rd), so the FIFO protects itself.
A common bug we fixed here:full = (wr_ptr - rd_ptr) == DEPTH looks right, but Verilog evaluates it at 32 bits, so after a wrap the difference comes out negative and full never sets. Our simulation confirmed the old version overflowed.
DEPTH must be a power of two for the wrap-bit trick.
Interview angle: Be ready for the corner cases: simultaneous read and write when full or empty, and what changes for a non-power-of-two depth.
A testbench that decides PASS or FAIL by itself, using a SystemVerilog queue as the reference model.
tb_fifo.v
// Self-checking testbench for sync_fifo: a queue is the reference model
`timescale 1ns/1ps
module tb_sync_fifo;
reg clk = 0, rst_n = 0, wr_en = 0, rd_en = 0;
reg [7:0] din = 0;
wire [7:0] dout;
wire full, empty;
reg [7:0] expected [$]; // reference model
integer errors = 0, n;
sync_fifo #(.WIDTH(8), .DEPTH(8)) dut (.*);
always #5 clk = ~clk;
task push(input [7:0] v);
begin
wr_en = 1; din = v; @(posedge clk); #1 wr_en = 0;
expected.push_back(v);
end
endtask
task pop_and_check;
reg [7:0] exp;
begin
rd_en = 1; @(posedge clk); #1 rd_en = 0;
exp = expected.pop_front();
if (dout !== exp) begin
errors = errors + 1;
$display("ERROR: read %h, expected %h", dout, exp);
end
end
endtask
initial begin
repeat (2) @(posedge clk); #1 rst_n = 1;
for (n = 0; n < 5; n = n + 1) begin // repeat so the pointers wrap
while (!full && expected.size() < 8) push($random);
if (!full) begin errors = errors + 1; $display("ERROR: full not set"); end
repeat (3) pop_and_check();
end
while (expected.size() > 0) pop_and_check(); // drain by the model, not the DUT
if (!empty) begin errors = errors + 1; $display("ERROR: empty not set"); end
if (errors == 0) $display("PASS");
else $display("FAIL: %0d errors", errors);
$finish;
end
endmodule
WHY IT'S WRITTEN THIS WAY
The queue mirrors what the FIFO should contain: push_back on every write, pop_front and compare on every read.
The loop fills the FIFO five times over, so the pointers wrap, which is exactly where the classic full-flag bug hides.
The final drain is driven by the model, not the DUT's empty flag, so a broken flag gives a clean FAIL instead of a hang. We checked this by breaking the DUT on purpose.
.* connects every port to a same-named signal, which keeps the instantiation short.
Interview angle: Talking about how you know your design works is as important as the design itself.
VerificationAdvancedNeeds an SVA/UVM simulator · not simulated here
SystemVerilog assertions for a req/gnt handshake
Three protocol assertions and a cover, attached to the design with bind, so the RTL file is never edited.
sva.sv
// SVA checker for a req/gnt handshake, attached to the design with 'bind'
module handshake_checker (
input logic clk,
input logic rst_n,
input logic req,
input logic gnt
);
default clocking cb @(posedge clk); endclocking
default disable iff (!rst_n);
// every new request is granted within 1 to 8 cycles
a_req_gets_gnt: assert property ($rose(req) |-> ##[1:8] gnt)
else $error("req not granted within 8 cycles");
// a request stays up until it is granted (no withdrawing)
a_req_held: assert property (req && !gnt |=> req)
else $error("req dropped before gnt");
// no grant without a request
a_no_spurious_gnt: assert property (gnt |-> req)
else $error("gnt without req");
// prove the interesting case actually happens in simulation
c_back_to_back: cover property (gnt ##1 $rose(req));
endmodule
// attach to every instance of 'arbiter_port' without editing the RTL
bind arbiter_port handshake_checker u_chk (.clk(clk), .rst_n(rst_n), .req(req), .gnt(gnt));
WHY IT'S WRITTEN THIS WAY
default clocking and default disable iff remove repetition from every property.
$rose(req) makes the first check start once per request, not on every cycle that req is high.
The cover property proves the back-to-back case really happens. An assertion that never triggers proves nothing.
Interview angle: Shows you understand assertions as living specifications, and how bind keeps verification code separate.
VerificationAdvancedNeeds an SVA/UVM simulator · not simulated here
UVM transaction with constraints, and a write-then-read sequence
A realistic UVM sequence item with layered constraints, and a sequence that writes an address and then reads it back.
uvm_seq.sv
// UVM transaction with constraints, and a sequence that uses it
class bus_txn extends uvm_sequence_item;
rand bit [31:0] addr;
rand bit [31:0] data;
rand bit write;
rand bit [3:0] burst_len;
constraint c_align { addr[1:0] == 2'b00; } // word aligned
constraint c_region { addr inside {[32'h0000_0000 : 32'h0000_FFFF]}; }
constraint c_burst { burst_len inside {[1:8]};
write -> burst_len <= 4; } // writes are short
constraint c_mix { write dist { 1 := 60, 0 := 40 }; }
`uvm_object_utils_begin(bus_txn)
`uvm_field_int(addr, UVM_ALL_ON | UVM_HEX)
`uvm_field_int(data, UVM_ALL_ON | UVM_HEX)
`uvm_field_int(write, UVM_ALL_ON)
`uvm_field_int(burst_len, UVM_ALL_ON)
`uvm_object_utils_end
function new(string name = "bus_txn");
super.new(name);
endfunction
endclass
class write_then_read_seq extends uvm_sequence #(bus_txn);
`uvm_object_utils(write_then_read_seq)
rand int unsigned n = 10;
constraint c_n { n inside {[5:20]}; }
function new(string name = "write_then_read_seq");
super.new(name);
endfunction
task body();
bus_txn wr, rd;
repeat (n) begin
wr = bus_txn::type_id::create("wr");
start_item(wr);
if (!wr.randomize() with { write == 1; })
`uvm_error("RAND", "write randomization failed")
finish_item(wr);
rd = bus_txn::type_id::create("rd");
start_item(rd);
if (!rd.randomize() with { write == 0; addr == wr.addr; }) // read back the same address
`uvm_error("RAND", "read randomization failed")
finish_item(rd);
end
endtask
endclass
WHY IT'S WRITTEN THIS WAY
Constraints stack: alignment, address range, burst length (with an implication for writes) and a read/write mix using dist.
Randomization happens between start_item and finish_item, and every randomize() result is checked.
The inline constraint addr == wr.addr ties the read to the previous write, which gives a self-checkable scenario.
Interview angle: Walk through the handshake line by line. It's the most common UVM coding question.
Moves a single-cycle pulse from a fast clock domain into a slow one without losing it.
pulsesync.v
// Pulse synchronizer (toggle-based): moves a single-cycle pulse from clk_a to clk_b.
// Works from a fast to a slow domain as long as pulses are spaced >= ~3 clk_b cycles apart.
module pulse_sync (
input wire clk_a, rst_a_n, pulse_a,
input wire clk_b, rst_b_n,
output wire pulse_b
);
// 1) turn each pulse into a level change (toggle) in the source domain
reg tgl_a;
always @(posedge clk_a or negedge rst_a_n)
if (!rst_a_n) tgl_a <= 1'b0;
else if (pulse_a) tgl_a <= ~tgl_a;
// 2) synchronize the level into clk_b, then 3) detect its edges
reg s1, s2, s3;
always @(posedge clk_b or negedge rst_b_n)
if (!rst_b_n) {s3, s2, s1} <= 3'b000;
else {s3, s2, s1} <= {s2, s1, tgl_a};
assign pulse_b = s2 ^ s3; // one clk_b pulse per toggle
endmodule
WHY IT'S WRITTEN THIS WAY
A 1-cycle pulse in the fast domain can fall between two slow-clock edges and vanish. Converting it to a toggle (a level change) means it can't be missed.
The toggle passes through a 2-flop synchronizer, and a third flop detects each change, which becomes one pulse in the slow domain.
Limitation: pulses must be spaced at least about three destination cycles apart. For bursts, use a handshake or an async FIFO. Our testbench sends 20 pulses from a 250 MHz to a ~71 MHz domain and receives all 20.
Interview angle: This is the textbook answer to "a pulse from a 100 MHz domain is sometimes missed in a 25 MHz domain. Why, and how do you fix it?"
Gray code changes exactly one bit per increment. That's what makes async FIFO pointers safe to synchronize.
gray.v
// Binary <-> Gray converters (parameterized, purely combinational)
module bin2gray #(parameter W = 4) (input wire [W-1:0] bin, output wire [W-1:0] gray);
assign gray = bin ^ (bin >> 1);
endmodule
module gray2bin #(parameter W = 4) (input wire [W-1:0] gray, output reg [W-1:0] bin);
integer i;
always @(*) begin
bin[W-1] = gray[W-1];
for (i = W-2; i >= 0; i = i - 1)
bin[i] = bin[i+1] ^ gray[i]; // each bit = XOR of all higher Gray bits
end
endmodule
WHY IT'S WRITTEN THIS WAY
Binary to Gray is a single expression: bin ^ (bin >> 1).
Gray to binary is a chain: each binary bit is the XOR of all the Gray bits above it, done here with a loop in always @(*).
The testbench checks the round trip for all 16 values, and that consecutive Gray codes differ in exactly one bit.
Interview angle: Know why Gray code works for counters but not for arbitrary multi-bit data.
ControlAdvanced✓ Simulated & self-checked
4-way round-robin arbiter
Grants a shared resource fairly: whoever was served last goes to the back of the line.
arbiter.v
// 4-way round-robin arbiter: the most recently granted requester gets lowest priority next
module rr_arbiter (
input wire clk,
input wire rst_n,
input wire [3:0] req,
output reg [3:0] gnt // one-hot, registered
);
reg [1:0] last; // index of the last grant
reg [3:0] next_gnt;
integer i;
reg [1:0] idx;
always @(*) begin
next_gnt = 4'b0000;
// search starting just after 'last', wrapping around
for (i = 1; i <= 4; i = i + 1) begin
idx = last + i[1:0];
if (req[idx] && next_gnt == 4'b0000)
next_gnt[idx] = 1'b1;
end
end
always @(posedge clk or negedge rst_n)
if (!rst_n) begin
gnt <= 4'b0000;
last <= 2'd3; // so requester 0 has priority first
end else begin
gnt <= next_gnt;
if (next_gnt[0]) last <= 2'd0;
else if (next_gnt[1]) last <= 2'd1;
else if (next_gnt[2]) last <= 2'd2;
else if (next_gnt[3]) last <= 2'd3;
end
endmodule
WHY IT'S WRITTEN THIS WAY
last remembers the previous winner. The search starts just after it and wraps around.
Only one grant is ever issued (next_gnt == 0 check), and the grant is registered for clean timing.
With all four requesting, the testbench sees grants rotate evenly (at least 9 each in 40 cycles) and always one-hot.
Interview angle: Follow-ups: scaling to N requesters, weighted round-robin, and the critical path of the priority search.
ControlIntermediate✓ Simulated & self-checked
Moore FSM: 1011 sequence detector (overlapping)
Detects the serial pattern 1011, including overlapping matches (so 1011011 contains two matches).
seqdet.v
// Moore FSM: detects the serial pattern 1011, overlapping matches allowed
module seq_1011 (
input wire clk,
input wire rst_n,
input wire din,
output wire found // high for one cycle after the last '1' of 1011
);
localparam S0 = 3'd0, // nothing matched
S1 = 3'd1, // "1"
S2 = 3'd2, // "10"
S3 = 3'd3, // "101"
S4 = 3'd4; // "1011" -> output state
reg [2:0] state, next;
always @(posedge clk or negedge rst_n)
if (!rst_n) state <= S0;
else state <= next;
always @(*) begin
case (state)
S0: next = din ? S1 : S0;
S1: next = din ? S1 : S2; // "11": the last '1' can still start a match
S2: next = din ? S3 : S0; // "100": no suffix matches a prefix of 1011
S3: next = din ? S4 : S2; // "1010": suffix "10" is a prefix
S4: next = din ? S1 : S2; // overlap: "10110" ends in "10", "10111" ends in "1"
default: next = S0;
endcase
end
assign found = (state == S4); // Moore: output depends on state only
endmodule
WHY IT'S WRITTEN THIS WAY
Each state records how much of the pattern has matched so far, using named localparam states.
The overlap transitions (S3 → S2 and S4 → S1/S2) come from asking which longest suffix of the input is still a prefix of 1011.
Separate sequential (state register) and combinational (next state) blocks, with a default to avoid latches.
The testbench feeds 1011011 and expects exactly two detections.
Interview angle: Almost every fresher interview asks for a sequence detector. Practise drawing the state diagram before coding it.
ControlBeginner✓ Simulated & self-checked
Parameterized priority encoder
Returns the index of the highest-priority (highest-numbered) active request.
prienc.v
// Parameterized priority encoder: highest-index set bit wins
module prio_enc #(parameter N = 8) (
input wire [N-1:0] req,
output reg [$clog2(N)-1:0] idx,
output wire valid
);
integer i;
assign valid = |req;
always @(*) begin
idx = 0;
for (i = 0; i < N; i = i + 1)
if (req[i]) idx = i; // later (higher) iterations overwrite earlier ones
end
endmodule
WHY IT'S WRITTEN THIS WAY
In a combinational for loop, later iterations overwrite earlier ones, so the last set bit found wins.
Synthesis unrolls the loop into a priority chain. For large N, a tree-based structure is faster.
valid is separate, because an index of 0 is ambiguous: bit 0 set, or nothing set?
Interview angle: Explain why the valid output is needed. It's a subtle point interviewers check.
PRACTISE
Try these without looking
Close this page and write each one from memory, then simulate it. Free simulators such as Icarus Verilog or Verilator (or EDA Playground in the browser) are all you need.
WARM-UP
Modify the counter
Add a terminal-count output that pulses when the counter wraps. Then make the counter saturate at a limit instead of wrapping.
CORE
Detect a different sequence
Change the FSM to detect 1101 (overlapping), and write a testbench that counts the matches in a random stream.
STRETCH
Make the FIFO asynchronous
Use the Gray converters and synchronizers from this page to give the FIFO separate read and write clocks.