90+ free SystemVerilog questions for design verification interviews: data types, arrays and queues, classes, virtual interfaces, constraints, covergroups, assertions, fork-join, mailboxes and scheduling. Includes 18 worked answers with code, 8 solved problems, a buggy transaction class reviewed line by line, a coverage-closure estimator and a rapid-fire round.
A questions-only bank from data types to UVM RAL, with 323 UVM questions and 170 debugging, company-style and expert questions.
SystemVerilog interviews for verification roles test two things at once: whether you know the language precisely (what a bit does to an X, what join_any leaves running, which method a base handle calls), and whether you can use it to build a testbench that finds bugs. The second half of most rounds is "write a constraint", "write a covergroup", "write an assertion", then "why doesn't it work?"
Start with the cheat sheet and the container table, skim the 50 questions by topic, then work through the worked answers and the output-tracing problems with a simulator open. Finish with the code review: reading someone else's class and spotting what's wrong is one of the most common senior DV exercises.
2-state vs 4-state, packed vs unpacked, dynamic arrays, queues and associative arrays, tasks vs functions, always_comb, fork-join variants. Short questions where the exact answer matters.
A class hierarchy with virtual methods, constraints for a packet, a covergroup with a cross, an assertion for a handshake, a mailbox between generator and driver.
"randomize() returns 0", "coverage stuck at 92%", "the test never ends", "passes on one simulator only". They want a method, and an understanding of the event regions.
A declaration and one use beats a paragraph. "int q[$]; q.push_back(5);" answers "what is a queue" faster than any definition.
Say which one you mean. Most OOP confusion (copies, nulls, polymorphism) disappears once handles and objects are kept apart.
Initial values, always_comb running once, X on uninitialised logic. Start-up behaviour is where many testbench bugs live.
A constraint that can't solve, a bin that can't be hit, an assertion that passes vacuously. Knowing how each feature fails shows you've used it.
Every feature exists to find bugs faster. Say what it's for: "associative arrays for a sparse memory model", "randc to cover every opcode once per pass".
Active, NBA, Observed, Reactive. You don't need every region, but you should know why clocking blocks and non-blocking assignments prevent races.
The rules behind most one-line questions, in one place.
logic, reg, integer, nets: 0, 1, X, Z · default value X (nets Z)bit, byte, shortint, int, longint: 0, 1 only · X and Z become 0Signed by default: byte, shortint, int, longint, integer. Unsigned: bit, logic, reg.[N] · dynamic [] + new[N] · queue [$] · associative [key_type]join: all children · join_any: first child · join_none: none · wait fork, disable forkrand: any legal value, repeats allowed · randc: every value once before any repeat (n bits → 2n per cycle):= weight per value · :/ weight shared across a range · P = weight / total weight|-> same cycle · |=> next cycle · ##[m:n] window · [*n] consecutive · $rose $fell $stable $pastb = a copies the handle · b = new a shallow-copies the object · deep copy needs your own methodHow many cross bins you've created, and roughly how many random samples it takes to hit them all if every bin is equally likely.
"Which would you use, and why?" follows almost every array question. Pick by how the size changes and how you look things up.
| CONTAINER | DECLARE | SIZE | BEST FOR | WATCH OUT FOR |
|---|---|---|---|---|
| Fixed array | int a[16]; | Compile time | Known sizes, register files, lookup tables | Can't grow; out-of-range reads return the default |
| Dynamic array | int d[]; d = new[n]; | Set at run time | Payloads whose length is randomised | Accessing before new[]; resize with new[m](d) to keep data |
| Queue | int q[$]; | Grows and shrinks | Scoreboard expected lists, FIFOs, in-order matching | Popping an empty queue; q[$] is the last element |
| Associative array | int m[int]; | Only entries you write | Sparse memory models, lookups by ID or address | Reading a missing key; check with exists() first |
| Mailbox | mailbox #(pkt) mb = new(); | Unbounded, or bounded by new(n) | Passing objects between threads (generator to driver) | get() blocks when empty, so a missing producer hangs the test |
A sample of what DV interviewers ask most. The 1,527-question ebook goes much deeper on every topic, plus six sections of UVM.
States, signs and widths.
bit but fail with logic?logic and wire when a signal has two drivers?enum help debugging, and what does .name() return?int to an unsigned 8-bit variable?Choosing and using containers.
find_index and with return on a queue?Blocks, tasks and functions.
always_comb check that always @(*) doesn't?@(posedge clk)?ref, and when by const ref?final block used for in a testbench?Connecting the testbench to the DUT.
input #1step mean in a clocking block?Handles, inheritance and polymorphism.
super.new() explicitly?virtual (abstract), and what is a pure virtual method?$cast check, and when do you need it?Constraints and the solver.
rand and randc?:= and :/ differ in a dist?solve a before b change the distribution, but not the solutions?Measuring what you tested.
ignore_bins and illegal_bins each do?Checking behaviour over time.
req rises, it stays high until gnt"?disable iff for reset?cover property tell you that assert can't?Fork-join, mailboxes, events.
join_any, and how do you stop it?fork inside a for loop?@ev and wait(ev.triggered)?put() is called on a full bounded mailbox?The parts that cause the strangest bugs.
import and `include?24 sections from data types to UVM RAL, with 65 debugging scenarios, 70 company-style questions and 35 expert corner cases. Questions only, so you practise producing answers.
Answer out loud (or in a simulator) first, then open each one. Every answer shows what a strong reply covers, what the interviewer is listening for, and the slip that costs the most.
logic: a 4-state variable. It can be driven by one continuous assignment or from procedural code, but not by multiple drivers.wire: a 4-state net. It resolves multiple drivers, so it's what you need for tri-state buses and inout ports.bit: a 2-state variable. Faster and smaller in simulation, but an X or Z assigned to it becomes 0.logic [7:0] a; // one driver, can hold X
wire [7:0] bus; // many drivers, resolved
bit [7:0] b;
initial begin
b = dut.data_out; // if data_out is X, b reads 0: the bug is hidden
endRule of thumb: use logic (or nets) for anything sampled from the DUT, so X propagation stays visible; use 2-state types for testbench-internal counters and indices.
byte payload[]; // dynamic: size chosen at run time
payload = new[len];
pkt expected[$]; // queue: push at one end, pop at the other
expected.push_back(p);
p = expected.pop_front();
bit [31:0] mem [bit [31:0]]; // associative: only touched addresses exist
mem[32'h8000_0000] = 32'hDEAD_BEEF;
if (mem.exists(addr)) rdata = mem[addr];fork
drive_stimulus();
wait_for_timeout(1000);
join_any // continue when either finishes
disable fork; // kill whichever is still running
fork
monitor_bus(); // keep running in the background
join_none // parent continues immediately
...
wait fork; // later: block until all children finishjoin waits for every child.join_any waits for the first child; the others keep running.join_none waits for nothing; children start when the parent next blocks.disable fork kills all child threads of the current process, including ones forked earlier, so wrap the fork in its own begin...end isolation thread if other children must survive.
Interfaces are static, elaborated in the module hierarchy; classes are dynamic, created at run time. A class can't contain an interface instance, but it can hold a handle to one: that's a virtual interface.
interface bus_if (input logic clk);
logic valid;
logic [7:0] data;
endinterface
class driver;
virtual bus_if vif; // set from the top before run
task drive(byte d);
@(posedge vif.clk);
vif.valid <= 1'b1;
vif.data <= d;
endtask
endclass
// top module: drv.vif = bus0; (in UVM: uvm_config_db#(virtual bus_if)::set)It also makes the driver reusable: the same class can drive any instance of bus_if.
class hdr; int id; endclass
class pkt;
int len;
hdr h = new();
function pkt deep_copy();
deep_copy = new();
deep_copy.len = len;
deep_copy.h.id = h.id; // copy the contents, not the handle
endfunction
endclass
pkt a = new(), b, c, d;
b = a; // same object: changing b.len changes a.len
c = new a; // shallow: new pkt, but c.h and a.h are the same hdr
d = a.deep_copy(); // fully independentIn a scoreboard this matters: if the monitor reuses one object and the scoreboard stores the handle, every stored entry changes when the monitor writes the next transaction. Store a copy.
always_comb runs once at time 0 (so outputs aren't stuck at X until an input changes), is sensitive to signals read inside called functions, forbids other processes writing its outputs, and lets tools warn when it would infer a latch.always_ff states the intent "flip-flops"; tools flag anything that isn't a single clocked event control.always_latch documents an intentional latch.always_ff @(posedge clk or negedge rst_n)
if (!rst_n) q <= '0;
else q <= d;
always_comb begin
y = b; // default first: no latch
if (sel) y = a;
endclass base_pkt;
virtual function void show(); $display("base"); endfunction
endclass
class err_pkt extends base_pkt;
function void show(); $display("error packet"); endfunction
endclass
base_pkt b;
err_pkt e = new();
b = e; // base handle, derived object
b.show(); // prints "error packet"
// without 'virtual' in base_pkt, prints "base"With virtual, the call is chosen at run time by the object's type; without it, at compile time by the handle's type. Polymorphism is what lets a driver handle a queue of base handles that actually point to many packet types, and what makes UVM factory overrides work.
class pkt;
rand bit [31:0] addr;
rand bit [7:0] len;
rand bit err;
constraint c_len { len inside {[1:64]}; }
constraint c_addr { addr inside {[32'h1000 : 32'h1FFF]};
addr[1:0] == 2'b00; }
constraint c_err { err dist { 1 := 1, 0 := 9 }; }
endclass
pkt p = new();
if (!p.randomize() with { len == 64; }) // corner case for one test
$fatal(1, "randomize failed");
p.c_err.constraint_mode(0); // turn one constraint offThen mention the tools for later tests: soft for defaults a test may override, solve...before when you need a skewed distribution, and always checking the return value of randomize().
typedef enum {RD, WR, RMW, NOP} cmd_e;
covergroup pkt_cg with function sample(bit [7:0] len, cmd_e cmd);
cp_len : coverpoint len {
bins small = {[1:8]};
bins mid = {[9:32]};
bins large = {[33:64]};
illegal_bins zero = {0};
}
cp_cmd : coverpoint cmd; // one bin per enum value
x_len_cmd : cross cp_len, cp_cmd {
ignore_bins no_nop = binsof(cp_cmd) intersect {NOP};
}
endgroup
pkt_cg cg = new();
// in the monitor, once per observed packet:
cg.sample(p.len, p.cmd);Explain the choices: explicit bins instead of 256 auto bins, an illegal bin for a value the spec forbids, and an ignored cross for a combination that can't happen. That's 3 × 4 − 3 = 9 cross bins.
property p_req_ack;
@(posedge clk) disable iff (!rst_n)
$rose(req) |-> ##[1:3] ack;
endproperty
a_req_ack : assert property (p_req_ack)
else $error("ack not seen within 3 cycles of req");
c_req_ack : cover property (p_req_ack);|-> starts checking in the same cycle as the trigger; |=> would start one cycle later.$rose(req) avoids starting a new attempt every cycle that req stays high.disable iff stops false failures during reset.cover proves the check ever fired; without it, an assertion that never triggers looks the same as one that always passes.interface bus_if (input logic clk);
logic valid, ready;
logic [7:0] data;
clocking cb @(posedge clk);
default input #1step output #1;
input ready;
output valid, data;
endclocking
endinterface
// driver
@(vif.cb);
vif.cb.valid <= 1'b1; // driven 1 time unit after the edge
if (vif.cb.ready) ... // value sampled just before the edgeInputs are sampled in the Preponed region, before anything changes at that edge, so the testbench always sees the value the DUT's flops saw. Outputs are driven a skew after the edge, so the DUT never sees a testbench change in the same delta it samples. Both sides stop depending on simulator ordering.
mailbox #(pkt) gen2drv = new(4); // bounded: put() blocks at 4 items
// generator thread // driver thread
repeat (n) begin forever begin
p = new(); gen2drv.get(p); // blocks if empty
void'(p.randomize()); drive(p);
gen2drv.put(p); end
end
semaphore bus_key = new(1); // one owner of a shared bus
bus_key.get(1); do_transfer(); bus_key.put(1);with constraints, a factory override with extra constraints, or state variables (non-rand fields) set to unusual values before the call.constraint_mode(0) until it solves.if (!obj.randomize()) `uvm_fatal(...) everywhere, so a failure stops the test instead of reusing old values.ignore_bins after checking with the designer, or illegal_bins if hitting them is a bug.dist, a dedicated sequence, or a directed test.cover property on the antecedent and the full property, and check it's hit in regressions.disable iff with the wrong polarity, disables the check permanently.bind to the wrong module or to an internal signal renamed by synthesis can leave the assertion unconnected.mailbox.get() or semaphore.get() waiting for something that will never come.@ev waits for the next trigger; if it fired earlier in the same time step, use wait(ev.triggered).forever loops started with join instead of join_none, or never killed.new() ever called? Declaring a handle doesn't create an object; arrays of handles need each element constructed.null, or a failed $cast used as a task (which errors) vs a function (which returns 0 and leaves the target unchanged).vif never set from the top or the config DB gives the same error at the first access.fork children, foreach on associative arrays with unusual keys, or uninitialised variables used before assignment.Fix the races first: drive and sample through clocking blocks, use <= for anything that crosses an edge, and remove time-0 ordering assumptions.
Output-tracing and probability questions are a favourite in written tests. Work each one out on paper, then tap to check.
len dist { 0 := 10, [1:4] := 5, [5:8] :/ 20 }; What is P(len = 0), P(len = 3) and P(len is 5 to 8)?Show solution ↓0 : 10 → 10 [1:4] : 5 per value → 20 [5:8] : 20 shared → 20 (5 per value) Total weight = 50 P(0) = 10/50 = 20% P(3) = 5/50 = 10% P(5..8) = 20/50 = 40%
Note: := gives every value in the range the full weight; :/ splits it.
fork #10 $display("A"); #5 $display("B"); join_any $display("C"); #20 $display("D");Show solution ↓t=5 B (first child finishes, join_any releases) t=5 C t=10 A (still running in the background) t=25 D (5 + 20)
Note: with join, C would print at 10 and D at 30; with join_none, C at 0 and D at 20.
int q[$] = {3, 7}; then push_front(1), push_back(9), insert(2, 5), x = pop_back(), delete(0). What are x, q.size() and q[1]?Show solution ↓push_front(1) → {1, 3, 7}
push_back(9) → {1, 3, 7, 9}
insert(2, 5) → {1, 3, 5, 7, 9}
x = pop_back() → x = 9, {1, 3, 5, 7}
delete(0) → {3, 5, 7}
size = 3, q[1] = 5Note: insert(i, v) puts v at index i and shifts the rest right.
req rises at cycle 5. In which cycles does $rose(req) |-> ##[1:3] ack look for ack? And with |=>?Show solution ↓|-> : consequent starts at cycle 5
##[1:3] → cycles 6, 7, 8
|=> : consequent starts at cycle 6
##[1:3] → cycles 7, 8, 9
Passes at the first cycle ack is seen; fails if none of the three.Note: a |=> b is the same as a |-> ##1 b.
rand bit a; rand bit [1:0] b; constraint c { a -> b == 0; } What is P(a = 1)? What changes with solve a before b?Show solution ↓Legal solutions: a=0 with b=0..3 (4), a=1 with b=0 (1) → 5 Default (uniform over solutions): P(a=1) = 1/5 = 20% solve a before b: pick a first, 50/50 P(a=1) = 50% P(b=0) = 0.5 × 1 + 0.5 × 1/4 = 62.5%
Note: solve...before never changes which solutions are legal, only how likely each is.
Fixed: 2^32 words × 4 bytes = 16 GiB, allocated up front
Associative: ~10,000 entries × (key + data + overhead)
→ well under a few MBNote: associative entries cost more each, but you only pay for addresses you touch. Use exists() to return a default for untouched addresses.
randc bit [1:0] c; The first three randomisations give 2, 0 and 3. What is the fourth? Can the fifth be 1?Show solution ↓Values per cycle: 2^2 = 4 → {0, 1, 2, 3}
Used: 2, 0, 3 → fourth must be 1
Fifth starts a new cycle: any of 0..3, including 1Note: randc only prevents repeats within a cycle; a value can repeat across the boundary.
Cross bins = 3 × 4 − 3 = 9
Expected samples ≈ n × H(n) = 9 × (1 + 1/2 + … + 1/9)
≈ 9 × 2.829 ≈ 25.5
~95% likely by ≈ n × (ln n + 3) ≈ 9 × 5.2 ≈ 47Note: this assumes every bin is equally likely, which real stimulus rarely is. The calculator above does the same sums.
"Here's a packet class from our testbench. What's wrong with it?" is a common senior DV exercise. This class should generate short packets most of the time and long packets when is_long is set. It has five problems.
// pkt.sv — transaction for a simple streaming bus class pkt; bit [7:0] len; (1) rand bit [31:0] addr; rand bit [7:0] data[]; rand bit is_long; constraint c_len { len inside {[1:16]}; } constraint c_long { is_long -> len > 32; } (2) constraint c_data { data.size() == len; } function void display(); (3) $display("addr=%h len=%0d", addr, len); endfunction function pkt copy(); copy = this; (4) endfunction endclass // in the generator p = new(); p.randomize(); (5) mb.put(p.copy());
len isn't rand. The solver treats it as a constant (0), so c_len and c_data can never be satisfied.c_long contradicts c_len. Once len is rand, the solver quietly sets is_long to 0 every time. No error, just a coverage hole.display() isn't virtual, so a derived packet's display() is never called through a base handle.copy returns the same handle. The mailbox and the generator share one object; the next randomisation changes what the driver is sending.randomize() is ignored. With bug (1) every call fails, and every packet goes out with the old values.(1) and (5) together: every randomize() fails and nobody notices, so the whole test sends the same packet. Then (4), which corrupts data in flight, then the silent contradiction in (2).
class pkt;
rand bit [7:0] len;
rand bit [31:0] addr;
rand bit [7:0] data[];
rand bit is_long;
constraint c_len { is_long -> len inside {[33:255]};
!is_long -> len inside {[1:16]}; }
constraint c_data { data.size() == len; }
virtual function void display();
$display("addr=%h len=%0d long=%0b", addr, len, is_long);
endfunction
virtual function pkt copy();
copy = new();
copy.len = len; copy.addr = addr;
copy.data = data; copy.is_long = is_long; // arrays copy by value
endfunction
endclass
if (!p.randomize()) $fatal(1, "pkt randomize failed");A coverpoint on is_long (or on length ranges) would have shown the long bin at 0% after any amount of regression. That's what coverage is for: finding stimulus that silently never happens.
A compare() method for the scoreboard, a soft default for is_long so tests can bias it, and, in UVM, field macros or do_copy/do_compare overrides. The ebook has full sections on OOP, randomisation and debugging questions like this.
Screening calls often open like this. Answer each in a few seconds, then tap to check.
X.
0.
Signed, 32-bit, 2-state.
The last element of the queue.
exists().
Nothing. The parent continues at once.
disable fork.
8 values, one full cycle.
No.
No.
Automatic.
Only static properties and other static methods.
Time-consuming statements: #, @, wait.
|->
|=>
When sig went from 0 to 1 since the last clock tick.
The simulator reports an error.
0, and the variables keep their old values.
NBA region, after Active and Inactive.
1 on success, 0 on failure.
A failed randomisation that nobody checks sends the same stimulus forever. Always check the return value.
Sampling DUT outputs into bit or int turns X into 0, and the bug you were looking for disappears.
"I copied it" when you copied a handle. Say which one you mean, every time.
Without it, a base handle calls the base method, and factory overrides seem to do nothing.
Blocking assignments to DUT inputs at the clock edge create races that depend on the simulator.
Auto bins on wide fields and covergroups nobody samples give numbers, not answers. Bins should come from the spec.
For verification roles: data types and arrays, classes and OOP, virtual interfaces, constrained randomisation, functional coverage, assertions, fork-join and inter-process communication, then debugging and scheduling questions. Design roles focus on the synthesisable subset instead.
SystemVerilog. UVM is a class library written in it, so classes, virtual methods, parameterised classes, constraints and fork-join need to be comfortable before UVM makes sense.
No. EDA Playground gives free browser access to several simulators, which is enough to run every example on this page. Open-source tools support a growing share of SystemVerilog too.
For most DV roles: implication, delays and windows, repetition, $rose/$fell/$past, disable iff, and the difference between assert and cover. Formal verification roles go much deeper.
2-state vs 4-state types, arrays and queues, tasks vs functions, classes with inheritance and virtual methods, a simple constraint and covergroup, fork-join variants, and how a basic testbench is structured.
Usually. Expect a class with constraints, a covergroup, an assertion, or a small generator-driver pair over a mailbox, followed by "what happens if…" questions.
Book a 1:1 mock interview with a mentor working in this exact domain.
Book a mock interview