System Verilog Interview Questions
In short: These SystemVerilog interview questions are grouped by topic and answered with real, worked code you can run. They cover data types, arrays, classes and OOP, randomization and constraints, threads and events, interfaces and clocking blocks, plus verification metrics. Each answer explains the why, not just the what, so you can defend it in an interview. Use this alongside the other interview questions to build a complete picture.
SystemVerilog extends Verilog with a full verification language: classes, randomization, functional coverage, and assertions. Interviewers test whether you understand these deeply enough to use them well. Below, each concept has a short answer and a code example. For more depth on any topic, follow the links into the SystemVerilog posts.
A simple way to approach SystemVerilog questions
Think of an interview like a code review. The interviewer is not just checking whether you know a keyword; they want to see that you know when to use it and what breaks if you use the wrong one. So for every answer, say what the feature does, then give the one-line reason it matters. A candidate who says “logic is 4-state, so it catches uninitialized X values that a 2-state bit would hide” sounds far stronger than one who just recites “logic can be 0, 1, X, Z.”
Data types
bit vs logic
A bit is 2-state (0 or 1). A logic is 4-state (0, 1, X, Z). Use bit for fast internal counters and scoreboard maths; use logic anywhere you drive or sample RTL, so uninitialized (X) and floating (Z) values are not silently hidden.
bit [7:0] count; // 2-state: starts at 0, fast
logic [7:0] data; // 4-state: starts at X, models real signals
initial begin
$display("count = %0d, data = %b", count, data);
// count = 0, data = xxxxxxxx (the X warns you data was never driven)
endWhy it matters: using bit on a DUT interface can mask a real bug where RTL never drove a signal, because bit would read 0 instead of X.
logic vs reg vs wire
- wire is a net; it must be driven continuously (by an assign or a port) and cannot hold a value on its own.
- reg is the old Verilog variable type, assigned inside procedural blocks. It does not imply a hardware register.
- logic is the SystemVerilog replacement for reg. It can be driven procedurally or by a single continuous assign, so you rarely need reg any more. The one limit: logic cannot have multiple drivers, so a multi-driver net still needs wire.
byte vs logic [7:0]
Both are 8 bits, but byte is a 2-state signed type, while logic [7:0] is 4-state and unsigned by default. Use byte for arithmetic on small values; use logic [7:0] when you need X and Z or a plain bit vector.
union vs struct
A struct stores all its members at once, each in its own space. A union stores one member at a time, all sharing the same space, so it is used to view the same bits in different ways or to save memory.
typedef struct packed {
bit [7:0] addr;
bit [7:0] data;
} pkt_s; // 16 bits total: addr and data both exist
typedef union packed {
bit [15:0] word;
bit [7:0] bytes [2];
} view_u; // 16 bits total, viewed as one word or two bytesDisplay and file tasks
$display vs $write vs $monitor vs $strobe
| Task | What it does |
|---|---|
| $display | Prints once, immediately, and adds a newline |
| $write | Same as $display but adds no newline |
| $strobe | Prints once at the end of the current time step, after all other updates that step |
| $monitor | Prints automatically every time any listed signal changes; only one $monitor is active at a time |
Why $strobe matters: in a time step where a value is still changing, $display may print the old value, while $strobe waits until the step settles and prints the final value.
Arrays
packed vs unpacked arrays
A packed array is a contiguous block of bits you can treat as one number; dimensions are written before the name. An unpacked array is a collection of separate elements; dimensions are written after the name. Use packed for bit vectors and arithmetic, unpacked for memories and collections.
bit [3:0][7:0] packed_reg; // 32 contiguous bits, usable as one value
bit [7:0] mem [0:255]; // 256 separate bytes, a small memoryWhich array type for a large memory (16/32/64KB)?
Use an associative array. A fixed unpacked array of 64KB allocates every location up front, wasting memory when the test touches only a few addresses. An associative array stores only the entries you actually write, so it models a huge sparse memory cheaply.
bit [7:0] sparse_mem [int]; // associative: only written addresses use memory
initial begin
sparse_mem[32'h1000_0000] = 8'hAB; // one entry allocated, not 64KB
if (sparse_mem.exists(32'h1000_0000))
$display("value = %h", sparse_mem[32'h1000_0000]);
endFind elements greater than 5 with the array locator “find”
int da[] = '{2, 7, 4, 9, 5, 11, 1};
int result[$];
result = da.find(x) with (x > 5); // returns the matching elements
$display("%p", result); // '{7, 9, 11}Resize a dynamic array from 50 to 100, keeping the lower 50
Call new[100] on the array but pass the old array as the initializer. That copies the existing elements into the new, larger array; the rest default to zero.
int da[];
da = new[50];
// ... fill da[0..49] ...
da = new[100](da); // grows to 100, preserves the first 50 valuesClasses and object-oriented features
private, public, and protected members
- public (the default) members are visible everywhere.
- local (SystemVerilog word for private) members are visible only inside the class that declares them.
- protected members are visible inside the class and its child classes, but not outside.
Why it matters: marking a field local or protected stops other code from changing internal state by accident, which keeps a verification component predictable.
new() vs new[]
new() is the class constructor; it builds one object. new[n] sizes a dynamic array to n elements. They look similar but do completely different jobs.
packet p;
p = new(); // constructor: create one packet object
int da[];
da = new[16]; // dynamic array: allocate 16 elementsForward declaration of a class
A forward declaration tells the compiler a class name exists before its full body is seen. It is needed when two classes refer to each other, so the first one can name the second before it is defined.
typedef class driver; // forward declaration
class monitor;
driver drv; // legal now, driver is known by name
endclass
class driver;
// full definition
endclassFunctions, tasks, and arguments
ref and const ref arguments
A ref argument passes the actual variable, not a copy, so changes inside the task are seen by the caller. A const ref passes by reference too but forbids modification, which is used to pass a large object efficiently without allowing the callee to change it.
task automatic add_one(ref int a); // caller sees the change
a = a + 1;
endtask
function automatic int sum(const ref int arr[]); // fast, read-only
sum = 0;
foreach (arr[i]) sum += arr[i];
endfunctionWhy const ref: passing a big array by value copies the whole thing; const ref avoids the copy while guaranteeing the function cannot corrupt the caller data.
System tasks and functions
System tasks and functions are built-in routines, all starting with a dollar sign, that the simulator provides. Examples: $display and $strobe (printing), $random and $urandom (random numbers), $time (current time), $fopen and $fwrite (file I/O), $finish (end simulation), and $sformatf (format a string).
Control flow
case vs casex vs casez
| Statement | How it treats X and Z |
|---|---|
| case | Exact match; X and Z must match exactly |
| casez | Treats Z (and ?) as don’t-care in the comparison |
| casex | Treats both X and Z as don’t-care |
Prefer casez over casex. casex treating X as don’t-care can hide real X bugs, because an unknown value silently matches a branch it should not.
Which equality operator is used inside case, casex, casez?
The case statements use the identity operator triple-equals (===) style matching, meaning they compare X and Z as real values (a case item must match bit for bit), not the logical double-equals (==) which returns X when either side has an X. That is why plain case needs exact X and Z matches.
forever loop vs for loop
A for loop runs a fixed number of times set by its counter. A forever loop runs endlessly and must contain a timing control (like @ or #) or a disable, so it is used for always-running processes such as a clock generator or a driver main loop.
// for loop: known count
for (int i = 0; i < 8; i++) drive_beat(i);
// forever loop: runs for the whole simulation
forever begin
@(posedge clk);
sample_signals();
endHow SystemVerilog simulation regions work
Each time step is split into ordered regions so that hardware-like behaviour is deterministic. The main ones, in order: Active (normal RTL and blocking assignments), then NBA (non-blocking assignment updates), then Observed (assertion evaluation), then Reactive (program blocks and testbench), then Postponed ($strobe and $monitor). Clocking blocks sample in the region that avoids races with the design.
Why regions exist: they stop testbench and design code from racing. A clocking block samples inputs in a safe region so the testbench sees stable values, not mid-update ones.
Interfaces and clocking blocks
Interfaces
An interface bundles a group of related signals into one named block, so a testbench and DUT connect through a single handle instead of dozens of individual ports. It can also hold clocking blocks, modports, and assertions.
interface bus_if(input logic clk);
logic valid, ready;
logic [31:0] data;
clocking cb @(posedge clk);
output valid, data;
input ready;
endclocking
modport TB (clocking cb);
endinterfacemodport
A modport gives a named view of an interface that fixes signal directions for one user. The DUT modport might drive ready, while the testbench modport drives valid; the same interface serves both without wiring mistakes.
Clocking block and its advantages
- Race-free sampling. Inputs are sampled at a defined time relative to the clock, so the testbench never sees mid-update values.
- Skew control. Input and output skews model realistic setup and hold behaviour.
- Cleaner code. Drive and sample through the clocking block instead of chasing signal timing by hand.
input #1 step vs input #1ns skew
In a clocking block, input #1step samples the signal one step before the clock edge, meaning at the end of the previous time step (the last settled value). input #1ns samples it a fixed 1 nanosecond before the edge. Use 1step for the standard race-free sample; use an explicit time only when you need a specific offset.
Randomization and constraints
pre_randomize and post_randomize
These are callbacks the solver runs automatically. pre_randomize() runs just before randomization, to set up values or turn constraints on and off. post_randomize() runs just after, to fix up or check the result.
class packet;
rand bit [7:0] len;
bit [7:0] crc;
function void post_randomize();
crc = ~len; // compute a derived field after randomizing
endfunction
endclassEnable or disable constraints selectively
Use constraint_mode() to switch a named constraint on or off, and rand_mode() to switch randomization of a variable on or off.
p.c_small.constraint_mode(0); // turn OFF the c_small constraint
p.addr.rand_mode(0); // stop randomizing addr (keep its value)
assert(p.randomize());unique constraint
The unique constraint forces a set of variables or all elements of an array to take different values in one randomize call.
class u_arr;
rand bit [7:0] vals [5];
constraint c_uniq { unique { vals }; } // all 5 elements differ
endclassDynamic array of unique random values, with and without the unique constraint
With unique it is one line. Without it, you constrain each element to differ from the earlier ones using foreach.
// With unique
rand bit [7:0] a [10];
constraint c1 { unique { a }; }
// Without unique (manual)
constraint c2 { foreach (a[i]) foreach (a[j]) if (i != j) a[i] != a[j]; }hard vs soft constraints
A hard constraint must always hold; if it cannot, randomization fails. A soft constraint is a default that the solver drops if it conflicts with a hard constraint or an inline constraint. Use soft for sensible defaults that a test may override.
class cfg;
rand int mode;
constraint c_def { soft mode == 0; } // default, overridable
endclass
cfg c = new();
assert(c.randomize() with { mode == 3; }); // soft default yields to thisOverride a base-class constraint in a child class
Declare a constraint with the same name in the child class. The child version replaces the parent one for objects of the child type.
class base;
rand int x;
constraint c_range { x inside {[0:10]}; }
endclass
class child extends base;
constraint c_range { x inside {[100:200]}; } // overrides base
endclassGenerate a packet with address greater than 500
assert(pkt.randomize() with { addr > 500; });Value with exactly 15 ones, no two adjacent ones
Constrain the population count to 15 and forbid any bit being 1 next to another 1 using a bitwise-and of the value with its shift.
rand bit [31:0] v;
constraint c_ones { $countones(v) == 15; }
constraint c_adj { (v & (v << 1)) == 0; } // no two adjacent 1sDynamic array: each element < 50 and size < 50
rand int da[];
constraint c_sz { da.size() inside {[1:49]}; }
constraint c_val { foreach (da[i]) da[i] < 50; }Array size 50 to 100 with values in descending order
rand int da[];
constraint c_sz { da.size() inside {[50:100]}; }
constraint c_desc { foreach (da[i]) if (i > 0) da[i] < da[i-1]; }Threads, events, and mailboxes
Counting parallel threads
fork…join starts each statement inside it as a separate thread. A for loop wrapped around a fork…join_none spawns one thread per iteration. To count threads, count how many statements sit directly inside the fork.
fork
taskA(); // thread 1
taskB(); // thread 2
begin // thread 3 (one begin-end block is one thread)
step1(); step2();
end
join
// 3 parallel threadsMerging two events
Assign one event to another with the merge operator so that triggering either one wakes waiters on both. This is done with the event variable assignment.
event e1, e2;
initial e2 = e1; // merge: e1 and e2 now refer to the same event
// -> e1; will wake anything waiting on e2 as wellbounded vs unbounded mailboxes
A mailbox passes data between threads. An unbounded mailbox has no size limit, so put() never blocks. A bounded mailbox is created with a size; put() blocks when it is full, which applies natural back-pressure between a fast producer and a slow consumer.
mailbox mb_u = new(); // unbounded: put never blocks
mailbox mb_b = new(4); // bounded to 4: put blocks when fullSign-off and project questions
Gate Level Simulation and why it matters
Gate Level Simulation (GLS) runs the netlist produced after synthesis, with real gates and often real timing, instead of RTL. It matters because it catches problems that RTL simulation cannot see: X-propagation from uninitialized flops, reset issues, timing violations with back-annotated delays, and mismatches introduced by synthesis. It is a standard sign-off check before tape-out.
Metrics to track verification progress
- Functional coverage percentage, from the functional coverage model, shows how much planned behaviour was exercised.
- Code coverage (line, branch, toggle, FSM) shows how much RTL the tests touched.
- Assertion pass and fail counts from assertions.
- Bug rate over time, which should fall as the design matures.
- Regression pass rate and number of consecutive clean runs.
When is verification complete?
Verification is complete when the sign-off criteria are all met: functional and code coverage at their goals with justified exclusions, all planned tests passing across several consecutive clean regressions, all known bugs fixed or waived with reasons, and the bug discovery rate flattened. Coverage at 100% with a flat bug curve is the practical signal of maturity.
How to check whether a given constraint is correct
Read it for three things: is the range reachable (no impossible combination), does it avoid contradicting another constraint, and does it use the right operators (for example inside for ranges, and dist for weighting). A common bug is writing overlapping hard constraints that leave no legal value, which makes randomize() fail. When in doubt, relax to a soft constraint or print the solver failure.
Keep practising
Work through these out loud with a whiteboard, then extend each answer with a small example of your own. Pair this page with the wider interview questions, and deepen any weak spot through the assertions and functional coverage guides.
Credit and further reading
These questions and answers are reframed from the knowledge shared by semiconductor industry experts Mr. Robin Garg and Mr. Ramdas Mozhikunnath, whose generosity in sharing with the VLSI community made this collection possible. Credit for the original insight goes to their work.
Books worth reading to learn SystemVerilog in more depth:
- Writing Testbenches using SystemVerilog by Janick Bergeron
- SystemVerilog for Verification by Chris Spear
- Verilog and SystemVerilog Gotchas by Stuart Sutherland
- SystemVerilog Assertions Handbook by Ben Cohen, Srinivasan Venkataramanan, Ajeetha Kumari
- Principles of Functional Verification by Andreas Meyer
- SystemVerilog Assertions and Functional Coverage by Ashok B. Mehta
- A Practical Guide for SystemVerilog Assertions by Srikanth Vijayaraghavan, Meyappan Ramanathan
- Introduction to SystemVerilog by Ashok B. Mehta
Frequently asked questions
What is the main difference between bit and logic in SystemVerilog?
A bit is a 2-state type (0 or 1) while logic is 4-state (0, 1, X, Z). Use bit for fast internal counters and maths, and logic wherever you drive or sample RTL, because logic keeps X and Z visible so uninitialized or floating signals are not silently read as 0.
When should I use an associative array instead of a fixed or dynamic array?
Use an associative array for large, sparse storage such as a 16, 32, or 64KB memory that a test touches only in a few places. It allocates memory only for the entries you actually write, unlike a fixed array that reserves every location up front.
What is the difference between hard and soft constraints?
A hard constraint must always hold, and randomization fails if it cannot be satisfied. A soft constraint is a default the solver drops when it conflicts with a hard or inline constraint. Use soft constraints for sensible defaults that individual tests can override.
What do pre_randomize and post_randomize do?
They are callbacks the solver runs automatically around randomization. pre_randomize() runs just before, useful for enabling or disabling constraints or setting up values. post_randomize() runs just after, useful for computing derived fields like a CRC or checking the result.
Why prefer casez over casex in SystemVerilog?
casez treats only Z (and ?) as don’t-care, while casex treats both X and Z as don’t-care. Because casex lets an unknown X silently match a branch, it can hide real bugs, so casez is the safer default.
What is a clocking block and why use it?
A clocking block groups signals with a clock and defines when they are sampled and driven relative to that clock. It gives race-free sampling, models setup and hold through skews, and keeps testbench timing clean, so the testbench never reads mid-update values.
What is Gate Level Simulation and why is it important?
GLS runs the synthesized gate netlist, often with real timing, instead of RTL. It catches issues RTL cannot show, such as X-propagation from uninitialized flops, reset problems, timing violations, and synthesis mismatches, which is why it is a standard sign-off step before tape-out.
When can you say verification is complete?
When all sign-off criteria are met: functional and code coverage reach their goals with justified exclusions, all planned tests pass across several consecutive clean regressions, every bug is fixed or waived with a reason, and the bug discovery rate has flattened.




Maybe stupid, regarding the definition of a wire
Wire a;
Assign a=en? In : a; // can be used to model a hold on a value when en==0
It’s a different way to use but it doesn’t mean wire holds a value just like reg.
hey, good job
Thank you Alin Mocanu π Please share with your friends so it will reach to everyone who seeks knowledge π
Excellent post. I was checking continuously this blog and I am impressed!
Extremely usefful information. I care for such innformation a lot.
I was looking for this certain information for a very long
time.Thahk you and goood luck.
Thank you π Please share this with your friends and network π
I have a question on code coverage.
there is a 4bit variable lets say [0:15]a, 0 to 11 values of code should get 100% coverage and 12 to 15 values must be excluded. How can we write this code coverage?
Hi Sreeja,
Are you sure you want to write code coverage? Can Verification Engineer write code coverage?
Thank you
Here a is not storing any value, its being driven continously
29. Given a 32 bit address field as a class member, write a constraint to generate a random value such that it always has 10 bits as 1 and no two bits next to each other should be 1
class packet;
rand bit[31:0] addr;
constraint c_addr {
$countones(addr) ==10;
foreach (addr[i])
if(addr[i] && i>0)
addr[i] != addr[i-1];
}
endclass
Great job!.
I don’t think this code works for the given question because foreach will be looking for a two dimensional array. Please correct me if I’m wrong.
It would be great if you post more problem-solving questions for constraints
Hi Kavya,
Thanks, for visiting the blog post. Can you please help me to know why it will look for a 2-dimensional array here and did you try this example in your code?
Excellent collection & explaination of all topics !! just Great.
Thanks Vicky π