Interviews
SystemVerilog Interview Questions
Practise the question, then the follow-ups an interviewer asks next.
Start with a Question TreeAI Mock Interview
- 37
- Question trees
- 215
- Follow-ups
- 296
- Wrong answers explained
- 74
- Runnable on a simulator
=) and a nonblocking (<=) assignment, and which one do you use for flip-flops?
WhyWhy Nonblocking Makes Process Order IrrelevantBreak ItBreaking a Clocked Block with a Blocking TemporaryWhat IfA Testbench That Drives on the Sampling EdgeCompareClocking Blocks vs Driving on the Falling EdgeWhat IfReading a Clockvar After the Raw Edge
+ 3 more follow-ups · Open this tree
Flagship Questions · With follow-ups
Question Trees
Each question opens into the follow-ups an interviewer asks next, the wrong answers people give, and what a fresher's and a senior's answer sound like.
-
Blocking vs Nonblocking Assignments
What is the difference between a blocking (=) and a nonblocking (<=) assignment, and which one do you use for flip-flops? 8 follow-ups · 11 pitfalls · 1 runnable- Asked at NVIDIA
-
The Two-Flop Synchronizer and MTBF
Why do we put two flip-flops in a row to bring a single-bit signal into a new clock domain? 8 follow-ups · 11 pitfalls · 4 runnable- Asked at NVIDIA
- Asked at Texas Instruments
- Asked at Apple
-
Dynamic Array, Queue or Associative Array
When do you use a dynamic array, a queue and an associative array in a testbench? Give one real use of each. 13 follow-ups · 14 pitfalls · 2 runnable- Asked at Google
-
Removing Every Match from a Queue
Write code that removes every5from anintqueue. A candidate wrote the loop below; what is left inqafterwards? 6 follow-ups · 7 pitfalls · 2 runnable -
The Scoreboard That Remembered Before Reset
A test applies reset in the middle of traffic. After reset, the first response for ID 3 fails although the waveform shows the DUT returned the right data for the new request. Here is the log. What is wrong? 5 follow-ups · 7 pitfalls · 1 runnable -
Import Versus Include
What is the difference betweenimport axi_pkg::*;and aninclude "axi_txn.svh"directive? How can the wrong choice break a testbench? 4 follow-ups · 7 pitfalls · 1 runnable -
Calling a C Function from the Testbench
A C reference model exposesint is_large(unsigned char len). Write the SystemVerilog side that calls it with an 8-bit burst length, and tell me which type you chose and why. 5 follow-ups · 9 pitfalls · 1 runnable- Likely at Analog Devices
-
One C Model, Two Agents
Your C reference model keeps its state in a static global. It worked with one agent; with two agent instances the predictions are wrong for both. Why, and how do you fix it? 7 follow-ups · 11 pitfalls · 2 runnable -
A Layered Testbench Before a Methodology
A team wants to move a block from pin-level directed tests to constrained random. Would you build a layered testbench in plain SystemVerilog first, or go straight to UVM? Defend the choice. 5 follow-ups · 8 pitfalls · 1 runnable -
What Interfaces and Modports Give You
Why use an interface instead of a list of ports, and what does a modport add on top? 5 follow-ups · 7 pitfalls · 1 runnable -
Two Drivers, One Bus
A bench has two ports, each with its own driver. Port 1 never sees traffic and port 0 shows corrupted beats. Here is the log. What do you check first? 5 follow-ups · 7 pitfalls · 1 runnable -
Input Skew of 1step vs Zero
A flop toggles at every rising edge. One clocking block samples it withinput #1step, another withinput #0. Read the waveform: what does each one record at each edge? 6 follow-ups · 6 pitfalls · 3 runnable -
A Copy That Shares Its Payload
A packet class holds a handle to apayloadobject. The scoreboard saves a copy of each packet, then the driver reuses its packet for the next beat. Write acopy()method that keeps the saved copy safe. 5 follow-ups · 8 pitfalls · 4 runnable -
Which Method Runs Through a Base Handle?
A base classtxnand a derivedwr_txnboth defineshow(). You call it through atxnhandle that points to awr_txnobject. Which version runs, and what changes ifshow()is virtual? 6 follow-ups · 9 pitfalls · 4 runnable- Asked at Google
- Asked at Apple
-
Recovering a Write Through a Base Handle
A mailbox carriesbase_txnhandles, and some of the objects arewr_txn. Write the receiving loop that processes writes and skips everything else. 5 follow-ups · 8 pitfalls · 2 runnable -
Static Members of a Parameterized Class
class box #(type T = int);has astatic int created;that its constructor increments. You construct twobox#(int)and onebox#(byte). What dobox#(int)::createdandbox#(byte)::createdread, and why? 7 follow-ups · 8 pitfalls · 2 runnable -
Forking Workers Inside a Loop
What does this print, and how do you make each worker see its own index? 6 follow-ups · 7 pitfalls · 2 runnable -
A Task That Waits on the Caller's Flag
Write a taskwait_for(flag)that blocks until a flag, set later by another process, goes high. Why does the obvious version with aninputargument hang? 5 follow-ups · 8 pitfalls · 1 runnable -
Join, Join Any and Join None
When does the parent resume afterjoin,join_anyandjoin_none? Predict the output of this block. 9 follow-ups · 11 pitfalls · 3 runnable- Asked at Google
-
The Timeout That Killed the Monitor
After a request-with-timeout task was added, a background beat counter in the same test stops counting. Here is the task and the log. What happened? 5 follow-ups · 6 pitfalls · 1 runnable -
The Event Nobody Heard
A producer triggers-> doneat time 0 and a consumer waits with@(done), also at time 0. Sometimes the consumer hangs forever. Why, and how do you make it robust? 6 follow-ups · 7 pitfalls · 1 runnable -
Three Items, One Object
A generator builds one packet object, then loops three times: setp.id,mb.put(p). The checker receives three packets that all have the last id. Why, and what is the rule? 5 follow-ups · 7 pitfalls · 1 runnable -
Rand Versus Randc
What is the difference between arandand arandcvariable? When would you pickrandc? 5 follow-ups · 8 pitfalls · 2 runnable -
Why Writes Became Rare
With this class, about how many of 900 bursts are writes? Why? How would you get roughly half? 5 follow-ups · 8 pitfalls · 2 runnable- Asked at Google
-
The Randomize Call That Failed Quietly
The class sayslen inside {[1:16]}and a test callsvoid'(p.randomize() with { len > 20; }). What happens? 5 follow-ups · 7 pitfalls · 3 runnable -
A Permutation Built by Constraints
Write constraints for an array of four integers that is always a permutation of 0, 1, 2 and 3. Then tell me how you would check your answer. 5 follow-ups · 6 pitfalls · 3 runnable- Asked at Micron
- Asked at Intel
-
A Random Lane Number Below Zero
A test picks a lane withlane = $random % 4;. What is wrong, and what would you write instead? 4 follow-ups · 6 pitfalls · 2 runnable -
Chasing a One-in-500 Failure
Walk me through how you reproduce and fix a test that fails on about one seed in 500 in the nightly regression. This is what the log gives you. 6 follow-ups · 10 pitfalls -
The Regions of a Time Slot
Walk through the regions of one simulation time slot. What runs in each, and why should a testbench writer care? 7 follow-ups · 9 pitfalls · 1 runnable -
The Monitor That Waited Zero Delays
A monitor wakes onposedge clk, waits#0to "let the design settle", then reads the DUT's output flop. The log shows every beat one cycle late. A colleague suggests a second#0. What is going on? 5 follow-ups · 6 pitfalls · 1 runnable -
Display, Strobe and Monitor at the Same Edge
What does this print? 5 follow-ups · 7 pitfalls · 3 runnable -
The Monitor That Copied to an Int
A read monitor copies the DUT'srdatainto anintbefore comparing it with the expected value. The DUT never reset the upper nibble. What does this print, and what has the monitor hidden? 6 follow-ups · 8 pitfalls · 2 runnable -
Is Logic a Wire or a Reg?
Islogica wire or a reg? When do you still have to writewire? 5 follow-ups · 9 pitfalls · 3 runnable -
Which Byte Is Index Zero?
What does this print? Then tell me when you would choose a packed array and when an unpacked one. 6 follow-ups · 7 pitfalls · 2 runnable -
The Negative Number That Was Not Smaller
What does this print, and why? 5 follow-ups · 7 pitfalls · 3 runnable -
Putting an Integer in an Enum
You read an opcode from a register as anintand need it in anop_tenum variable. How do you assign it, and what can go wrong? 5 follow-ups · 7 pitfalls · 3 runnable -
The Unknown Select That Picked a Side
The select comes out of reset as x. What does this print, and what would real hardware do? 5 follow-ups · 7 pitfalls · 3 runnable
No question matches these filters.
Quick revision · One flat list
Question Bank
All 187 SystemVerilog questions, main and follow-up. Open a track, then a topic. Answer out loud first, then open the short answer.
Jump to a Track
187 questions · 35 topicsSystemVerilog
Blocking vs Nonblocking Assignments7
What is the difference between a blocking (=) and a nonblocking (<=) assignment, and which one do you use for flip-flops? L0L1
A blocking assignment (=) updates its target at once. So the next statement in the same block already sees the new value. A nonblocking assignment (<=) reads its right-hand side now, but schedules the update for the NBA region. That region runs after every process woken by the edge has read the old values. Use <= for flops in clocked blocks, so the result does not depend on process order. Use = for combinational logic and temporaries.
Two clocked always blocks read each other's outputs. Why does <= make the result independent of which block the simulator runs first? L1
With <=, neither block changes a variable during the Active region; each only reads and schedules. Whichever block runs first, both read the same pre-edge values. The updates all land later, in the NBA region. With =, the first block to run changes its output before the second reads it. So the order decides the answer.
A clocked block computes tmp = a + b; and then sum <= tmp;. Another always block also reads tmp at the same edge. What can go wrong? L1L2
Inside the block the code is fine, because tmp is a local temporary. The bug is the second reader: it may run before or after the first block in the same Active region. So it sees either the old or the new tmp. Keep blocking temporaries local to their block, or declare them inside it. Share only flop outputs written with <=.
The testbench does @(posedge clk) start = 1; and the DUT samples start in always_ff @(posedge clk). Sometimes the DUT reacts a cycle early. Why? L1L2
The testbench process and the DUT flop wake on the same edge, in the same region, and their order is unspecified. If the testbench runs first, the DUT samples the new start on that same edge. That is a cycle earlier than real hardware would. Drive after the edge, through a clocking block or with <=, so the DUT always samples the pre-edge value.
Compare a clocking block with driving stimulus on the falling edge as ways to avoid testbench races. L2
Driving on the falling edge puts drives and samples in different time steps, so it is simple and race-free. But it only works for single-edge designs with a known duty cycle. A clocking block samples inputs just before the active edge (#1step, the Preponed region). It drives outputs after a set skew, so the testbench stays cycle-based on the real edge. Clocking blocks scale to agents and interfaces; falling-edge drives suit small directed benches.
A monitor waits for @(posedge clk) and then reads cb.rdata. It sometimes reports the previous beat. What happened? L2
Clockvars (the sampled signals of a clocking block) are updated when the clocking event is processed, in the Observed region. A process that resumes on the raw posedge clk can read cb.rdata before that update. It then gets the previous sample. Wait on @(cb) instead, so the read happens after the clockvars change.
You lead verification for a block with several UVM agents. How do you keep the testbench-to-DUT boundary race-free across the whole team? L3L4
Every agent touches the DUT only through clocking blocks in its interface. Drivers write clockvars with <=, and monitors wait on the clocking event and read sampled values. No class code reads or writes raw DUT signals at an edge. Assertions sit in the interface or are bound to the DUT. Lint rules catch blocking writes in clocked blocks, and reviews treat raw signal access in a driver as a defect.
The Two-Flop Synchronizer and MTBF7
Why do we put two flip-flops in a row to bring a single-bit signal into a new clock domain? L0L1L2
A flop that samples an asynchronous input can violate setup or hold time and go metastable, hanging between 0 and 1. It then settles to 0 or 1 after an unbounded time. The second flop gives the first almost a whole destination period to settle before any logic uses the value. MTBF (mean time between failures) grows exponentially with that settling time. The pair does not make the value right on a given cycle. It makes a clean 0 or 1 overwhelmingly likely.
QuestionPractise The Two-Flop Synchronizer and MTBF →Why is a single flop not enough to synchronize an asynchronous input? L0L1
With one flop, logic reads its output in the same cycle that it might be metastable. Only the slack left after that logic is available for settling. Different gates can even read the same hanging value as different levels. The second flop isolates that settling from all other logic, and gives it about a full period.
Follow-upPractise Why One Flop Is Not Enough →What if the clock is so fast that one period is not enough settling time? What do you change? L1L2
MTBF grows exponentially with settling time, so a third flop adds a full period and multiplies MTBF. Compute MTBF from the flop's library data at the worst corner. Add stages until it is far beyond the product lifetime. Each stage also adds one cycle of latency to the crossing.
Follow-upPractise When One Period Is Not Enough Settling Time →What if the source clock is faster than the destination clock and the signal is a one-cycle pulse? L1L2
The pulse can rise and fall between two destination edges and never be sampled. One fix is to hold the signal for at least about 1.5 destination periods. Another is to turn the pulse into a toggle, synchronize the level, and turn each change back into a pulse. For events that must not be lost or merged, use a handshake.
Follow-upPractise A Fast Pulse into a Slow Clock Domain →Compare a Gray-coded value with a request/acknowledge handshake for carrying a multi-bit value across clock domains. L2
Gray code changes one bit per step, so a value caught mid-change is either the old or the new one. It suits counters that move by one, such as FIFO pointers. A handshake holds any data stable while a synchronized request crosses and an acknowledge comes back. It carries any value, but takes several cycles per transfer. Choose Gray for streaming pointers, and a handshake or FIFO for occasional wide data.
Follow-upPractise Gray-Code Pointers vs a Handshake for Multi-Bit Crossings →How can a two-flop synchronizer fail even though its two flops are exactly right? L2
Combinational logic in front of the first flop can glitch, and the destination may catch the glitch. Synchronizing one signal in two places lets the two copies resolve differently. Signals synchronized separately and then combined again lose any timing relationship between them. Logic between the two flops eats the settling time.
Follow-upPractise Breaking a Two-Flop Synchronizer Without Touching Its Flops →How do you sign off clock domain crossings on an SoC with dozens of clocks? L3L4
Start with structural analysis: define the clocks and their relationships. Then let the tool find every crossing and check that each has the right synchronizer for its type. Next comes protocol checking: each scheme's rules, such as hold times and Gray steps, become assertions in formal and simulation. Add reconvergence checks and reviewed waivers. Use metastability injection in simulation to expose designs that assume exact timing.
Follow-upPractise CDC Sign-Off on a Many-Clock SoC →Dynamic Array, Queue or Associative Array10
When do you use a dynamic array, a queue and an associative array in a testbench? Give one real use of each. L0L1
A dynamic array gets its size at run time and keeps it until you resize it. It fits a packet payload whose length is known when the packet is built. A queue grows and shrinks at both ends, so it fits an in-order scoreboard of expected items. An associative array stores only the keys you write. It fits a sparse memory model, or open requests looked up by transaction ID.
QuestionPractise Dynamic Array, Queue or Associative Array →Why does reading a missing associative-array key return a default instead of stopping the simulation? L1
The language treats it like an out-of-range read of any array. It returns the element type's default and may print a warning, so every expression still has a value. Stopping would make every speculative lookup fatal. The cost is that a check must call exists() when "absent" and "present with value 0" mean different things.
Compare appending one element at a time to a queue with growing a dynamic array by d = new[d.size() + 1](d). L1L2
The resize form allocates a new array and copies every element on each call. Building n elements this way costs about n squared copies. push_back on a queue is designed for growth at the end. Use the dynamic array when the size is known once, and the queue when elements arrive one by one.
What if the end-of-test report walks the ID table with foreach and prints the outstanding IDs? In what order do they come out, and can you delete while walking? L1L2
Keys come out in ascending key order, not insertion order; with signed int keys the standard puts negative keys first. Deleting entries inside the same foreach is undefined, so collect the keys first and delete afterwards.
Your bus allows out-of-order responses between IDs but keeps order within one ID. How do you structure the expected-data store, and what does it cost? L2L3
Use an associative array keyed by ID, where each element is a queue. It matches the ordering rule exactly: look up the ID, compare with the front of its queue, and pop. Lookups stay cheap however many IDs are open, and a non-empty queue at the end is an outstanding request to report. Store handles in the queues rather than copies, and flush the whole table on reset.
Follow-upPractise Choosing the Data Structure for an Out-of-Order Scoreboard →A test applies reset in the middle of traffic. After reset, the first response for ID 3 fails although the waveform shows the DUT returned the right data for the new request. Here is the log. What is wrong? L1L2
The scoreboard still holds the expected data of the request that was in flight at reset. So it compares the new response with a0. The DUT legitimately dropped that request; the testbench did not. On reset, flush every expected-data structure, and decide whether a dropped request should be logged or counted.
Why is it legal for the DUT to forget requests at reset instead of finishing them? L1
Reset returns the design's state to its defined initial values, and the queues holding outstanding requests are part of that state. Most protocols say transactions in progress at reset are abandoned and that nothing may be signalled until reset is released. The testbench must model the same loss.
Follow-upPractise Why a Reset Drops Outstanding Requests →What if responses can outlive reset, and the next request reuses the same ID? L1L2
Then a late response for the old request can match the new request with the same ID. The scoreboard may pass wrong data, or flag correct data. Give each expected entry extra identity, or wait a quiet period after reset before reusing IDs. Test it with a reused ID and a duplicate response.
Follow-upPractise A Response That Outlives Reset →Compare flushing the scoreboard on reset with tagging each expectation with a reset epoch number. L2
Flushing is simple and correct when the monitors also stop at reset. Tagging every entry with the epoch it was created in lets the checker recognise a late response from before the reset. It can then report that case as its own error instead of mismatching it. Use the epoch scheme when responses can still emerge from pipelines around the reset edge.
Follow-upPractise Flushing the Scoreboard vs Tagging Epochs →How do you make mid-test reset work across a whole environment with several agents, scoreboards and running sequences? L3
Give reset one owner that broadcasts it, and make every stateful component handle it. Drivers drop their current item and idle the bus, monitors abandon partial transactions, scoreboards flush or bump an epoch, and sequences restart. Then cover reset at each interesting point, such as mid-burst or with a full FIFO. Each component's reset code runs only in those cases.
Follow-upPractise Reset in the Middle of a Test Across a Whole Environment →Removing Every Match from a Queue5
Write code that removes every 5 from an int queue. A candidate wrote the loop below; what is left in q afterwards? L0L1
It leaves 5 3 7. Each delete(i) shifts the later elements down by one. So the second of two neighbouring 5s lands at index i, and the loop moves past it. Write the result instead of editing in place: q = q.find(x) with (x != 5);. If you must delete in a loop, walk the indexes from the end down to 0.
Compare q.find(x) with (x != 5) with a loop that pushes the survivors into a new queue. L1
Both build a new queue and both are linear. The locator method states the intent in one line and cannot get an index wrong. The loop can do more in one pass, such as count and log the removed items. In reviews, prefer the method unless the loop does more than filter.
Follow-upPractise Locator Methods vs a Hand-Written Loop →Break it: a loop deletes matches in place with q.delete(i) while i counts up. What goes wrong? L1
After q.delete(i), every later element moves down one index. The loop then adds 1 to i and skips the element that moved into slot i. So two matches in a row leave the second one behind. Loop down from the end, or build a new queue.
What if they count set flags with flags.sum() on a bit flags[8], or total byte lengths with lens.sum()? Predict the output. L1L2
It prints flags.sum()=0 with int=8 and lens.sum()=-56 with int=200. A reduction method returns the element type, so eight 1-bit flags add up modulo 2 and two signed bytes overflow. Cast each item in a with clause, such as flags.sum() with (int'(item)), to make the sum wide enough.
A scoreboard calls exp_q.pop_front() when a response arrives. How does this break when the DUT sends a response nobody asked for? L1
Popping an empty queue returns the element type's default, perhaps with a warning, and the scoreboard compares the response against that default. A spurious response of value 0 then passes. Check exp_q.size() first and report an unexpected response.
Import Versus Include5
What is the difference between import axi_pkg::*; and an include "axi_txn.svh" directive? How can the wrong choice break a testbench? L1L2
An include directive pastes text, so every file that includes a class declares its own copy. Copies declared in different scopes are different types, and you cannot assign one to the other. import makes the names of one compiled package visible, so every importer shares one type. Include class files only inside their package, and import the package wherever the classes are used.
The two item classes are character-for-character identical. Why does the compiler refuse to assign one to the other? L1L2
Class types match by declaration, not by their text. Each declaration creates its own type, with its own static members and its own place in the hierarchy. If the compiler matched classes by text, a change to one copy would silently change the meaning of code using the other. Verilator 5.052 reports it as "expects a CLASSREFDTYPE 'item', got CLASSREFDTYPE 'item'", which looks absurd until you see the two scopes.
Follow-upPractise Why Two Identical Classes Are Two Types →A module does import apb_pkg::*; import axi_pkg::*; and both packages define config_t. When does it fail, and how do you fix it? L2
Importing both is legal; it fails only where config_t is referenced, because the name is visible from two packages and neither wins. Write axi_pkg::config_t at the use, or import that one name explicitly with import axi_pkg::config_t;.
Compare import pkg::*;, import pkg::name; and writing pkg::name at each use. L1
A wildcard import offers every name, but only where nothing local claims it first. An explicit import brings one name in as if it were declared there. A local declaration of the same name is then a compile error, not a silent winner. A scoped reference pkg::name needs no import and names the source at every use. It is the clearest choice in shared code.
Several teams will reuse your AXI and APB agents in one SoC bench. How do you organise the packages and files? L2L3
Use one package per agent, holding its transaction, driver, monitor and configuration. Include the class files only there, and put nothing at compilation-unit scope. Shared types live in a small common package that the agent packages import. The dependency graph then has no cycles, and compile order is fixed. Interfaces stay outside packages, because a package cannot contain them.
Follow-upPractise Package Structure for Reusable Verification IP →Calling a C Function from the Testbench6
A C reference model exposes int is_large(unsigned char len). Write the SystemVerilog side that calls it with an 8-bit burst length, and tell me which type you chose and why. L1L2
Declare import "DPI-C" function int is_large(input byte unsigned len); and call it like any function. SystemVerilog byte is signed and maps to C char, so an unsigned C parameter needs byte unsigned, which maps to unsigned char. Include the simulator-generated header in the C file, so a type mismatch fails the build instead of passing wrong values.
Why bother including Vtb__Dpi.h in the C file when the code compiles without it? L1
The simulator writes a C prototype for every import into that header. With it included, the C compiler compares your definition against the import and refuses a mismatch such as char against unsigned char. Without it, the linker matches names only, so a wrong type or direction runs and produces wrong numbers.
What if the C side is a C++ file and you forget extern "C" on the definition? L1L2
The C++ compiler mangles the function name. The linker then looks for the plain C name and does not find it. So the build stops with an undefined reference at link time. Add extern "C" to the definition.
Compare a pure import, a plain import and a context import. When do you need each? L2
A pure function promises its result depends only on its arguments, with no side effects. So the simulator may skip or reuse calls. A plain import may keep state and have side effects but must not call back into SystemVerilog. A context import may call exported SystemVerilog routines and ask for its calling scope. This costs some optimisation, so use it only where C calls back.
What if the C model needs a whole packet of bytes, whose length varies? L2
Declare the argument as an open array, input byte unsigned data[], and C receives an svOpenArrayHandle. It asks the handle for the bounds with svSize, svLeft and svRight, and gets element pointers with svGetArrElemPtr1 using SystemVerilog indexes. Those pointers are valid only during the call, so copy anything the model must keep.
A C model stores the const char* it receives for a test name and prints it at the end of the test. Sometimes it prints garbage. Why? L2
A string argument reaches C as a pointer to memory the simulator owns, valid only while the call runs. After the call returns, the simulator may reuse or free that memory, so the stored pointer can point at anything. Copy the text into storage the C side owns during the call.
Follow-upPractise A C Model That Kept a String Pointer →One C Model, Two Agents7
Your C reference model keeps its state in a static global. It worked with one agent; with two agent instances the predictions are wrong for both. Why, and how do you fix it? L2L3
A static global is one variable for the whole simulation, so both agents' calls update the same state. Give each instance its own state. A C model_new() returns a pointer, SystemVerilog stores it in a chandle, and every call passes it back. Add reset and free entry points too, so a mid-test reset and the end of test leave nothing stale behind.
Why store the C pointer in a chandle rather than in a longint? L2
A chandle always has the size of a pointer on the machine that runs the simulation. The language only lets you assign it, compare it with null and pass it to C. That stops SystemVerilog code from doing arithmetic on it or inventing values. A longint would work by accident on a 64-bit host and offer none of those guarantees.
Compare an import and an export. When would the C model need an export? L2
An import lets SystemVerilog call C; an export lets C call a SystemVerilog function or task. The model needs an export when it must call into the testbench. For example, it can report a mismatch through the testbench's error counter, or read a configuration value. C may call an export only from inside a context import call.
What if the C model needs to wait for ten clock cycles in the middle of its work? L2L3
C itself cannot consume simulation time. Make the import a context task, and have C call an exported SystemVerilog task such as wait_cycles(10), which may contain @(posedge clk). The calling SystemVerilog thread blocks until the exported task returns, while the rest of the simulation runs on.
What if the reference model is written in Python? How does it reach the testbench? L2L3
The standard defines DPI only between SystemVerilog and C. So a small C bridge starts the Python interpreter, calls the model and converts arguments and results. The bridge must catch every Python exception and turn it into a status code, because nothing may unwind into the simulator. Pass a whole packet per call, not one byte, because each call into Python is much slower than a C call.
Follow-upPractise A Python Model Behind DPI →The architecture team owns a C++ model of the whole subsystem. How do you use it as the scoreboard's reference without forking it? L3
Wrap it behind a thin, versioned DPI layer owned by verification: transaction in, predicted transactions out, one handle per instance, plus reset. Feed it from monitors, never drivers, so it predicts from what the DUT actually received. Agree on where the model is untimed, such as ordering between independent streams, and compare there by key rather than by cycle.
Follow-upPractise Integrating an Architecture Model at Subsystem Level →What if a model class holds a chandle, and someone copies the object with new this or a copy() method? L2
The copy gets the same address, so both objects drive one C model. An update through either is seen by both. If the copy needs its own state, call a C clone function that allocates a new struct. Then decide which object deletes which model.
Follow-upPractise Copying an Object That Holds a Chandle →A Layered Testbench Before a Methodology5
A team wants to move a block from pin-level directed tests to constrained random. Would you build a layered testbench in plain SystemVerilog first, or go straight to UVM? Defend the choice. L3
Either way the bench needs the same layers. A driver and a monitor own the pins through an interface, a generator makes transactions, and a monitor feeds the scoreboard. Plain SystemVerilog first suits a small block or a team new to classes. Every handshake stays visible, and each layer maps onto a UVM agent later. For reusable IP, or a team that already has UVM in place, go straight to UVM. Otherwise you rebuild its factory, configuration and phasing by hand.
QuestionPractise A Layered Testbench Before a Methodology →Why should the scoreboard get its transactions from a monitor instead of from the driver that sent them? L2
The driver reports what it meant to send; the monitor reports what actually appeared on the pins. A driver bug that drops or corrupts a beat is invisible if the scoreboard trusts the driver. A monitor also works in a passive agent, where no driver exists, so the same checking reuses at subsystem level.
Follow-upPractise Why the Monitor Feeds the Scoreboard →Break it: the monitor reads DUT outputs directly at @(posedge clk), with no clocking block. What can it get wrong? L2
The monitor and the DUT flops then run at the same edge. The monitor may read a value before or after another process writes it, so it can log the wrong cycle's data. A clocking block samples its inputs at a fixed point before the edge. Then every read is the value from before the edge.
Follow-upPractise A Monitor That Reads at the Edge →Compare a hand-built layered bench with a UVM one. What do you get from UVM that you would otherwise write yourself? L2L3
UVM adds a factory, so tests can swap classes without editing the bench. It adds a configuration database and phases with objections for start-up and end of test. It also gives standard TLM ports and one reporting system. The cost is a large library, a learning curve and slower compiles. A plain bench has none of that cost and none of that reuse.
Follow-upPractise What UVM Adds to a Layered Bench →Two blocks you verified separately are now connected in a subsystem. How much of each block bench do you reuse, and how? L3L4
At the internal boundary the drivers go away, because the other block drives those pins, so those agents become passive monitors. Their scoreboards and protocol checks keep running unchanged on real traffic. External interfaces keep active agents, and a subsystem scoreboard checks end-to-end behaviour from the outer monitors. Reuse is cheap only if the block benches kept checking out of the drivers.
Follow-upPractise Reusing a Block Bench at Subsystem Level →What Interfaces and Modports Give You5
Why use an interface instead of a list of ports, and what does a modport add on top? L0L1
An interface bundles a protocol's signals in one place. It can also hold clocking blocks, tasks and assertions. Adding a signal then means one edit instead of changing every port list. A modport declares one side's view of that bundle, such as which signals a driver writes and a monitor only reads. The compiler then rejects a write through an input-only modport, which a plain interface port never checks.
QuestionPractise What Interfaces and Modports Give You →Why can't a driver class simply contain the interface instance? Why the virtual keyword? L1
Interfaces are static, elaborated hierarchy, while class objects are created at run time, so a class cannot instantiate one. A virtual interface is a variable holding a reference to an existing interface instance. The bench assigns it before use, so one driver class can be pointed at any instance of the interface.
Follow-upPractise Why Classes Need Virtual Interfaces →The test crashes with a null access the first time the driver touches vif.valid. What happened and how do you fail faster? L1
The virtual interface variable is null until something assigns it, and nothing did. Check it where it is received, for example right after the configuration lookup, and stop with a message naming the agent. The failure then points at the missing assignment, not at the first signal access.
Follow-upPractise A Virtual Interface That Was Never Set →Compare passing a bus as an interface with passing it as a packed struct port. L1L2
A packed struct port is a plain vector with named fields. Every field flows in one direction, and it synthesises like any port. An interface can mix directions through modports and can carry clocking blocks, tasks and assertions, which is why testbenches use it. Many RTL teams use structs for one-way buses and interfaces where both directions travel together.
Follow-upPractise An Interface vs a Packed Struct Port →Your team has twelve agents on the same bus protocol. Where do the protocol assertions live, and why? L2L3
Inside the interface, or in a checker bound to it, so every instance of the bus is checked in every bench. Each agent then reuses the checks with no extra work, and a block-level bench and a chip-level bench check the same rules. Give the checks a switch for the rare test that drives illegal traffic on purpose, rather than deleting them.
Follow-upPractise Putting Protocol Checks in the Interface →Two Drivers, One Bus3
A bench has two ports, each with its own driver. Port 1 never sees traffic and port 0 shows corrupted beats. Here is the log. What do you check first? L1L2
Check which interface instance each driver's virtual interface points at. Here both drivers got port0, so they overwrite each other and port1 stays idle. A virtual interface is a reference, so copying one handle shares the bus. Print each agent's interface path at the start of the run, and give every agent its own handle from the top level.
Compare handing each driver its virtual interface through the constructor with fetching it from a configuration database. L2
A constructor argument is explicit and checked at the call site, which suits a small hand-built bench. A configuration object or database lets deep components get the handle without every parent passing it down. UVM benches work this way. The cost is indirection: a wrong path or key leaves the handle null, so check it on receipt.
Follow-upPractise Passing the Interface by Constructor or by Configuration →The DUT grows to sixteen ports, generated by a parameter. How do you connect sixteen agents without copy-paste bugs like this one? L3
Create the interfaces in a generate loop indexed by port number. Publish each handle under a name built from the same index. Build the agents in a loop that reads the handle with that index. Then add a start-of-test check that the sixteen handles are all different and not null. A binding mistake then stops the run at time zero.
Follow-upPractise Binding Sixteen Agents to Sixteen Ports →Input Skew of 1step vs Zero3
A flop toggles at every rising edge. One clocking block samples it with input #1step, another with input #0. Read the waveform: what does each one record at each edge? L1L2
#1step samples in the Preponed region, so it records the value the flop held before the edge: 0, 1, 0 at the three edges. #0 samples in the Observed region, after the flop's NBA update, so it records the new value: 1, 0, 1. A monitor almost always wants #1step, because that is the value a real flop on the same clock would capture.
Why do clocking blocks have an output skew, and when does a drive with output #2 reach the wire? L1L2
The output skew sets when a drive written to a clockvar appears on the signal, relative to the clocking event. With output #2, a drive requested at the edge at 5 reaches the wire at 7. That is well clear of the edge, so the DUT samples it at the next edge. It also models a real launch delay when a model needs one.
The same bench now runs against a gate-level netlist with SDF delays. What changes in your clocking blocks? L3
With real clock-to-output delays, outputs change some time after the edge. A #1step input still reads the old value safely. But a monitor must allow for the new value arriving later in the cycle. You can set input skews to match the hold window you want to check. Output skews must respect the DUT's setup and hold times, so the bench does not cause violations itself.
A Copy That Shares Its Payload5
A packet class holds a handle to a payload object. The scoreboard saves a copy of each packet, then the driver reuses its packet for the next beat. Write a copy() method that keeps the saved copy safe. L1L2
new src makes a shallow copy. It copies every property, but for a handle property it copies only the handle. So both packets still share one payload. A deep copy also constructs a new payload and copies its contents. Each class writes its own copy(), and pkt::copy() calls payload::copy() for its handle members, so the chain grows with the class tree.
Why is a copy needed at all? What does this print? L0L1
It prints h1.v=2. A class variable holds a handle, a reference to an object, and h2 = h1 copies only the handle. Both names now refer to the one object new created, so a write through either is seen through both.
A generator creates one txn before its loop, sets a new address each pass and puts it in a mailbox. The driver receives three transactions, all with the last address. Why? L1
A mailbox stores what it is given, and for a class that is a handle. All three messages refer to the same object, which holds the address written last. Construct a new object inside the loop for every transaction.
Follow-upPractise A Generator That Reused One Object →A crc_pkt extends pkt and adds a crc field, but does not override the virtual copy_from(pkt rhs). The scoreboard copies packets through pkt handles. What breaks? L2
The call is virtual, but crc_pkt has no override, so the base body runs and copies only id. The copy looks right in every base field while crc keeps whatever it held before. Every class that adds data must override the copy method. It calls super for the inherited fields and casts rhs to reach its own.
Compare a copy(rhs) method that fills an existing object with a clone() method that returns a new one. L2
copy(rhs) writes into an object the caller already owns. Each class copies its own fields and calls super.copy(rhs) for the rest. It needs a $cast of rhs to reach the derived fields. clone() constructs the right derived type, usually by calling copy() on a fresh object, and returns it through a base handle. Methodologies such as UVM provide both on the same pattern.
Which Method Runs Through a Base Handle?6
A base class txn and a derived wr_txn both define show(). You call it through a txn handle that points to a wr_txn object. Which version runs, and what changes if show() is virtual? L0L1
Without virtual, the call is bound by the handle's declared type, so txn::show() runs. With virtual, it is resolved at run time by the object's type, so wr_txn::show() runs. This run-time choice is called polymorphism. It lets a driver written for base handles keep working when a test adds new derived transactions.
Compare a pure virtual method in a virtual class with a virtual method that has an empty body. L1
A pure virtual method has no body. Its class must be declared virtual, which means abstract: nobody can construct it. A subclass that does not implement the method cannot be constructed either. An empty virtual body compiles everywhere and silently does nothing when a subclass forgets the override. Use pure virtual for methods every subclass must provide, and an empty body for optional hooks.
How do constructors work along txn and rd_txn extends txn? What happens if rd_txn::new never calls super.new? L1
Constructing a rd_txn runs the base constructor first, through super.new(...), which must be the first statement. If you leave it out, the compiler inserts super.new() with no arguments. That fails to compile when the base constructor has a required argument, and silently uses the defaults when the argument has one.
Every rd_txn in the log has id=0, although the test constructs them with new(7, 'h40). The constructor sets addr correctly. What is missing? L1L2
The derived constructor never calls super.new(id), so the compiler inserted super.new(), which used the default id = 0. The id argument of rd_txn::new is just a local name that nothing reads. Add super.new(id); as the first statement.
If virtual methods are so useful, why doesn't SystemVerilog make every method virtual? L1L2
A non-virtual method is a promise from the base class. Callers that use a base handle always get that exact behaviour, whatever a subclass does. It also lets the compiler call the method directly instead of looking it up per object. The base-class author decides which behaviour is meant to be replaced, and marks only those methods virtual.
Follow-upPractise Why Methods Are Not Virtual by Default →A test wants error-injecting transactions on one agent only, without editing the driver or the generator. How do you design for that? L2L3
The generator creates every transaction in one place that a test can override, such as a factory. The driver only calls virtual methods on base handles. The test extends the transaction, overrides the virtual hooks that inject the error, and registers the subclass for that one agent. UVM's factory with an instance override is this pattern packaged, which is the main reason to use it at scale.
Follow-upPractise Swapping Transaction Types Without Editing the Driver →Recovering a Write Through a Base Handle5
A mailbox carries base_txn handles, and some of the objects are wr_txn. Write the receiving loop that processes writes and skips everything else. L1
You cannot assign a base handle to a derived handle without a cast. So call $cast(w, b) as a function and branch on its result. On success, w refers to the same object through the derived type; nothing is converted. On failure, $cast returns 0 and leaves w as it was, so code that ignores the result keeps using the previous write.
Why can the compiler accept b = w; but not w = b;? L1
Every wr_txn is a base_txn, so storing a derived handle in a base variable is always safe. A base variable might hold any subclass, or a plain base_txn. Only the object present at run time can say whether it is a wr_txn. So the check has to happen at run time, which is what $cast does.
What if the base handle is null? Does $cast(w, b) fail? L1L2
No: casting a null handle succeeds and sets the destination to null, because null is a legal value of every class type. A loop that treats success as "I have a write" then dereferences null. Check for null before the cast when a null can arrive.
Follow-upPractise A Cast That Succeeds on Nothing →Compare a consumer that tests each object with $cast against putting a virtual process() method on the transaction classes. L2
A virtual process() keeps behaviour with the data, so a new transaction type needs no change in the consumer. A $cast chain puts the knowledge in the consumer. Every new type means editing every chain, and a forgotten branch silently skips objects. Casting is still right when the consumer needs fields that only one subclass has, such as a scoreboard comparing write data.
Your subsystem scoreboard handles twelve transaction subtypes through long $cast chains, and each new type has broken it once. How do you restructure it? L3
Move the knowledge about each type into the type. Each subclass overrides virtual compare and convert2string. The scoreboard matches by key and calls them through base handles. Keep one explicit cast, with an error on failure, at the boundary where the monitor hands objects over. Add a check that every subtype produced by the monitors is covered, so a new type cannot be silently ignored.
Static Members of a Parameterized Class6
class box #(type T = int); has a static int created; that its constructor increments. You construct two box#(int) and one box#(byte). What do box#(int)::created and box#(byte)::created read, and why? L2
They read 2 and 1. Each distinct set of parameter values makes a separate class type, called a specialization. Each specialization has its own copy of every static member. So a parameterized class cannot hold one counter for all its specializations. Put that state in a non-parameterized base class or a package.
QuestionPractise Static Members of a Parameterized Class →Why doesn't the language share statics across all specializations of a parameterized class? L2
A type parameter can change the type and size of members, including statics declared with T. So box#(int) and box#(string) may not even have compatible statics. The language therefore treats every specialization as a separate class, generated from one template, and the statics follow.
What if you want one counter shared by every specialization, such as a global transaction ID? L2
Put the static in a base class without parameters, and derive the parameterized class from it. Derived classes share the static properties of their base class. So every specialization then updates the same counter. Reach it through the base class name, such as item_base::next_id.
Port 0's agent writes to 2004 although it was configured with base address 1000. Port 1 was configured with 2000. The agent stores its base address in a static int. What happened? L1L2
A static member is one variable per class, shared by every instance. Configuring port 1 overwrote the value port 0 had set. Make the setting an instance member, or pass a per-instance configuration object.
Follow-upPractise Two Agents, One Static Setting →A driver class holds a handle to a sequencer, and the sequencer holds a handle back to the driver. Compare typedef class with reordering the file to make this compile. L1L2
Reordering cannot work, because each class names the other before it is declared. typedef class sequencer; declares the name first, so driver can hold a sequencer handle. The full class can follow later in the same scope. Reordering is enough only when the dependency runs one way.
You are designing a reusable scoreboard. Do you parameterize it by transaction type, or write it against a common base transaction class? L3
A type parameter gives full type safety and needs no casts. But scoreboard#(axi_txn) and scoreboard#(apb_txn) share no type, so they cannot sit in one array. A base-class design can hold any transaction and swap types at run time. The price is a cast wherever code needs derived fields. Many teams combine them: a parameterized class that extends a non-parameterized base holding the common interface.
Forking Workers Inside a Loop5
What does this print, and how do you make each worker see its own index? L0L1
It prints worker 3 three times. With join_none, the forked processes start only when the parent blocks at wait fork. By then the loop has finished, and the one loop variable i holds 3. Give each pass its own copy: declare automatic int k = i; inside the fork, before the statement, and print k.
The statements inside the fork run after the loop. Why does automatic int k = i; still get 0, 1 and 2? L1
Declarations at the top of a fork block are initialised when the fork statement runs, in the parent. This happens before any child process starts. So each pass creates a new k holding that pass's i, and the child that later runs refers to its own k.
Without the copy, every child prints 3, not 2. Why the value that ended the loop? L1
The loop stops when i becomes 3 and the test fails. The children of join_none start only when the parent blocks, which is after the loop has finished. At that point i holds 3, the value that ended the loop. So every child prints 3.
Compare a static and an automatic task when two threads call each at the same time. What does this print? L1L2
Both static calls report id 2. A static task has one set of arguments shared by every call, and the second call overwrote the first while it waited. The automatic calls report 1 and 2, because each call gets its own arguments. Tasks in modules are static unless declared automatic; class methods are always automatic.
What if the wait fork is removed, or the loop is moved into a task that the test calls? What does wait fork actually wait for? L1L2
Without it, the initial block ends while the workers still run. Nothing waits for them, so a test can finish in the middle of their work. wait fork waits for every child process of the calling process. That includes children forked earlier for other reasons, such as a background monitor that never ends. Wrap the workers in their own fork begin ... end join when you only mean them.
A Task That Waits on the Caller's Flag5
Write a task wait_for(flag) that blocks until a flag, set later by another process, goes high. Why does the obvious version with an input argument hang? L1
An input argument is copied when the task is called. So the task waits on its own frozen copy and never sees the caller's variable change. Declare it ref bit f, which passes a reference, and make the task automatic, because ref is only allowed in automatic subroutines. Add const (const ref) when the task must read but never write the variable.
Why does the language forbid ref arguments in static tasks? L2
A static task has one shared argument slot for all calls. Two overlapping calls would each want that slot to refer to a different variable, and the second would silently re-point the first. Automatic tasks allocate a fresh slot per call, so every reference stays tied to its own caller.
Follow-upPractise Why Ref Arguments Need an Automatic Subroutine →What if you pass a large array as const ref? What does that buy, and what can still change it? L1L2
const ref passes the array without a copy, and the compiler refuses writes to it inside the routine. The routine still reads the caller's variable. So changes the caller makes while the routine runs are visible to it. It cannot change the array by mistake.
Compare a task with a function. Could wait_for have been a function? L0L1
A function runs in zero simulation time and may not contain #, @ or wait, so wait_for must be a task. A function can return a value and appear inside an expression. A task returns results through output arguments and is called as a statement. Use a void function for zero-time work with no result, so the compiler guarantees it never blocks.
What if the task reads a ref array, waits a few cycles, then reads it again for a checksum? L1L2
The second read sees whatever the caller wrote in between, because a reference is live. If the task needs a stable snapshot, copy the array into a local variable at the start. Or pass it by value.
Follow-upPractise A Ref Argument That Changed Under the Task →Join, Join Any and Join None7
When does the parent resume after join, join_any and join_none? Predict the output of this block. L0L1
join resumes when every child has finished, join_any when the first one does, and join_none at once without waiting. Here the parent resumes at t=1 with the fast child, the slow child still finishes at t=3, and late=1 prints at t=5. join_any stops the waiting, not the other children.
After a request-with-timeout task was added, a background beat counter in the same test stops counting. Here is the task and the log. What happened? L1L2
disable fork kills every active descendant of the calling process, not just the fork above it. The task runs in the test's process, so it also killed the counter the test had forked earlier. Put the race in its own scope, fork begin ... end join around it, so disable fork only reaches the response and timeout branches.
Why did the task's disable fork reach a process the task never created? L1L2
Calling a task does not start a new process; the task's statements run in the caller's process, as if pasted in. So the children of "the current process" include everything the caller had forked before the call. Only fork creates new processes.
What if you must keep disable fork in the task? How do you make it stop only the task's own children? L1L2
Wrap the race in its own process: fork begin ... end join. Inside that block, the race's children are the only children of the new process. A disable fork there stops only them. The caller's monitors keep running.
Compare killing the timeout through a process handle with using disable fork. L2
Inside each branch, process::self() returns a handle, and calling kill() on the timeout's handle stops exactly that process and its descendants. disable fork is shorter but reaches everything the current process has forked. Handles also let you await() a process or check its status(), which suits library code that must not touch the caller's threads.
A regression runs hundreds of sequences in one simulation. How do leftover timers and watchers from join_any races show up, and what rule prevents them? L3
They fire later, in an unrelated sequence. A stale timeout reports an error, or a watcher takes an event meant for someone else. The failure depends on which sequences ran before. The rule is that every race ends with an explicit cleanup in its own scope, fork begin ... end join around the race plus disable fork. End-of-test checks then confirm that no sequence-owned processes are still alive.
Compare begin #5 a = 1; #5 b = 1; end with the same two statements inside fork ... join. When does b become 1? L0
In begin ... end the statements run in sequence, so b is set at t=10. In fork ... join, each statement is its own process, and all start at the same time. Both delays count from t=0, so b is set at t=5. The fork still finishes only when both are done.
The Event Nobody Heard5
A producer triggers -> done at time 0 and a consumer waits with @(done), also at time 0. Sometimes the consumer hangs forever. Why, and how do you make it robust? L1L2
An event trigger wakes only processes already waiting at that moment, and two processes at time 0 run in an unspecified order. If the producer runs first, the trigger is lost and @(done) waits for one that never comes. Wait with wait (done.triggered), which stays true for the rest of the time step. When the message must last longer, use a flag or a mailbox.
Why are named events designed as momentary edges instead of flags? L1
An event means "something happened now", like a clock edge. Every process waiting at that moment wakes together, and nothing has to clear the event. A flag would need someone to decide when to reset it, and a reset race is just as hard. When the information must persist, a variable, a semaphore or a mailbox is the right tool.
Follow-upPractise Why Events Have No Memory →Why does the triggered property reset at the end of the time step instead of staying set? L1L2
triggered exists only to remove the race within one time step. If it stayed set, a later wait would wake on an old trigger. The event would then be a flag, and someone would have to clear it. For facts that must last, use a variable or a counter.
Compare a named event, a semaphore and a mailbox for telling a checker that a transaction is ready. L1L2
An event signals a moment and carries nothing, so it suits "start now" among processes that are already waiting. A semaphore counts keys, which suits sharing a resource, such as one bus between two drivers. A mailbox queues the transaction itself, so a slow checker loses nothing; it is the usual choice for passing data between components.
Follow-upPractise Event, Semaphore or Mailbox →What if the consumer starts waiting one clock cycle after the producer triggers the event? L2
wait (done.triggered) misses it too, because the triggered state is cleared at the end of the time step. When the waiter can arrive later, record the fact in something persistent. Set a bit and wait on it, put a message in a mailbox, or use a semaphore key.
Three Items, One Object5
A generator builds one packet object, then loops three times: set p.id, mb.put(p). The checker receives three packets that all have the last id. Why, and what is the rule? L1L2
A mailbox of a class type stores handles, not copies. So all three entries point to the one object, and every receiver sees its final state. Construct a new object for each item, or clone it before put. The general rule: once a handle is published to another component, the sender must not modify that object again.
Compare new() with new(1) for the mailbox between a generator and a driver. L1L2
An unbounded mailbox lets the generator run far ahead of the driver. It may create thousands of items before the first one is sent, so stimulus cannot react to DUT state. A mailbox of size 1 blocks put until the driver takes the item, so the two stay close. Choose the bound to match how far ahead stimulus may be decided.
Why does SystemVerilog pass class handles around instead of copying objects? L1
Objects can be large and can contain other objects. Copying on every hand-off would cost time and memory, and the right depth of copy differs from case to case. Handles make sharing cheap and explicit. The price is that the programmer decides when a real copy is needed.
Follow-upPractise Why Channels Pass Handles →What if the generator must reuse one object? How do you make the mailbox hand-off safe? L1L2
Put a copy in the mailbox, not the working object. Write a copy method that copies every field, including nested objects. Then the consumer owns its item, and the next randomize() cannot change it. A new object per transaction is simpler, and it is the usual rule.
A checker loops on if (mb.try_get(t)) check(t); with no delay. How does this break the simulation? L2
When the mailbox is empty, the loop never blocks. It spins forever in the same time step, so time can never advance. Use the blocking get, or add an event control such as waiting for the next clock when polling is really needed.
Rand Versus Randc6
What is the difference between a rand and a randc variable? When would you pick randc? L0L1
A rand variable is drawn independently on every call, so a value can repeat straight away. A randc variable is cyclic. It visits every allowed value once, in random order, before any value repeats. Then it starts a new cycle. Pick randc for a small set you want covered quickly, such as an opcode or a port number. Use rand for everything else.
The opcode field is randc, yet the log shows the same opcode twice in a row. The generator does tr = new; tr.randomize(); for every transaction. Why? L1
The cycle state lives in the object, and every new starts a fresh cycle with its first value chosen at random. Randomize one generator object repeatedly and copy its result into each transaction, or keep the randc field in a long-lived object.
Why not declare a 32-bit address randc? L1
The solver has to remember which values of the current cycle are used, which costs memory per value of the range. So the standard lets tools limit the width of a randc variable, but not below 8 bits. A 32-bit randc may be rejected and is not portable. For addresses, a constraint plus coverage does the job.
Why are randc variables solved before the rand variables, and what can that break? L2
To keep its cycle, the solver picks the randc value first, from the values not yet used. Then it solves the rand fields around it. If a constraint ties the rand fields to that value and no solution exists, the call fails. So limiting a randc field through other fields is fragile.
You must visit seven specific register addresses, in random order, exactly once each. Compare randc with filling an array and calling shuffle(). L1L2
randc cycles over every value its type and constraints allow. Seven scattered addresses therefore need an index variable, a range constraint and a lookup table. An array of the seven addresses with shuffle() states the intent directly. It works for any element type and gives a new order on every call. Use shuffle() for an explicit list, and randc when the set is a small contiguous range.
A colleague proposes making every enumerated field randc "so coverage closes automatically". What do you tell them? L2L3
randc guarantees one field's values cycle, but bugs live in combinations and sequences: opcode with length, back-to-back writes, a command during reset. Those are crosses and transitions that no per-field cycle controls. Close coverage by measuring the coverage model, finding holes and steering constraints or adding directed sequences at them.
Why Writes Became Rare5
With this class, about how many of 900 bursts are writes? Why? How would you get roughly half? L1L2
About 100, one in nine. The solver picks uniformly among all legal combinations. A read pairs with any of eight beats values, but a write pairs only with 0. So 1 of the 9 legal pairs is a write. solve is_write before beats chooses is_write first with equal odds and gives about half, without changing which pairs are legal.
Why does the standard define the distribution over whole solutions rather than giving each random field its own fair odds? L1L2
Constraints tie fields together, so there is no single "fair" order in which to pick them. Making every legal combination equally likely gives one clear answer. It does not depend on how the constraints are written or how the solver works inside. The price is that a constraint touching one field can skew another, and solve...before and dist exist to correct that.
Compare x dist {0 := 1, [1:3] := 1} with x dist {0 := 1, [1:3] :/ 1}. How often is x zero in each? L1L2
:= gives every value in the range the full weight, so 1, 2 and 3 weigh 1 each and 0 comes up about 1 time in 4. :/ splits the weight across the range, so 1, 2 and 3 share a weight of 1 and 0 comes up about half the time. A dist on is_write is another way to correct the bias, because it sets that field's odds directly.
What if a dist lists a value that another constraint forbids, say x dist {0 := 5, [1:3] := 1} together with x != 0? L2
By the standard the forbidden value simply drops out, and the weights apply among the legal values. Verilator 5.052 instead fails some of those calls, so the bug shows up as randomization failures on one tool only. Keep every value in a dist legal under the other constraints, or move the weighting into a separate field.
What are the limits of solve...before? L2
It changes only probabilities: the legal set, and therefore any conflict, is unchanged. The variables it orders must be rand; randc variables are not allowed, since they are already solved first. And the result is only as good as your tool. So you still measure the bias with coverage.
The Randomize Call That Failed Quietly5
The class says len inside {[1:16]} and a test calls void'(p.randomize() with { len > 20; }). What happens? L1
The constraints contradict each other, so randomize() returns 0 and every random variable keeps its previous value. With the result cast away, the test sends the old packet again as if it were new. No checker complains, because that packet is legal. Always test the return value and turn a 0 into an error naming the object and the call.
Compare constraint dflt { soft len == 8; } with constraint dflt { len == 8; } when a test calls randomize() with { len == 3; }. L1
The soft default yields: the call succeeds with len = 3. The hard default conflicts with the inline constraint, so the call returns 0. Write defaults that tests are expected to override as soft, and rules the hardware requires as hard.
What if two soft constraints conflict, one in the base class and one inline? L1L2
Soft constraints have priorities. Between two soft ones, the derived, inline or later one wins, and the other is dropped. So an inline soft len == 4 beats a base class soft len == 8. A hard constraint still beats both.
A class with thirty constraint blocks fails to randomize in one test. How do you find the conflict? L2
Read the tool's unsatisfied-constraint report first, if it gives one; it often names the blocks involved. Otherwise switch blocks off with constraint_mode(0) and bisect until the call succeeds. Then look at what the last disabled block fights with, including the test's inline with clause and any field frozen by rand_mode(0).
An error test froze addr with rand_mode(0) to force an illegal address. The next test reuses the object and every randomize() call fails. Why? L1L2
rand_mode(0) lasts until someone turns it back on. So addr is still fixed state, and its value 41 breaks the hard rule addr < 32. Every call fails and leaves 41. Restore rand_mode(1) when the directed case ends, or give each test a fresh object.
A Permutation Built by Constraints5
Write constraints for an array of four integers that is always a permutation of 0, 1, 2 and 3. Then tell me how you would check your answer. L0L1
Keep every element in range and make them all different: foreach (a[i]) a[i] inside {[0:3]}; and unique {a};. Four distinct values from a set of four must be a permutation. Check it by randomizing many times and OR-ing 1 << a[i] over the elements: the mask must be 4'b1111 on every call.
Constrain an 8-bit x to have exactly one bit set, without using $onehot. L0L1
Write x != 0 && (x & (x - 1)) == 0;. Subtracting 1 clears the lowest set bit and sets the bits below it. So the AND is zero only when at most one bit was set. x != 0 removes the zero case. $countones(x) == 1 says the same thing more readably.
Compare unique {a}; with foreach (a[i]) foreach (a[j]) if (i < j) a[i] != a[j];. L1
They allow exactly the same solutions. unique states the intent in one line and also accepts a mix of scalars and arrays, such as unique {a, b, c};. You write the nested loop on an older tool. Its i < j guard stops it from comparing an element with itself.
Constrain a five-element array to be strictly increasing, with every element between 1 and 20. L1
Use a range plus a guarded neighbour rule: foreach (a[i]) { a[i] inside {[1:20]}; if (i > 0) a[i] > a[i-1]; }. The guard keeps index 0 from referring to an element that does not exist. The solver then picks uniformly among all increasing sequences, rather than sorting random values afterwards.
Now make the elements bit [4:0] and add a.sum() == 50. What can go wrong? L1L2
The sum() method returns the element type, so the total is computed in 5 bits and wraps at 32. The solver may then satisfy the constraint with a sum that is 50 modulo 32, or fail to find one. Write a.sum() with (int'(item)) == 50 so the addition happens in 32 bits.
A Random Lane Number Below Zero4
A test picks a lane with lane = $random % 4;. What is wrong, and what would you write instead? L0L1
$random returns a signed 32-bit value, so $random % 4 is negative about half the time and gives lanes from -3 to 3. Use $urandom_range(3, 0), which returns an unsigned value in the inclusive range. Inside classes, randomize() and std::randomize() are better still, because they take constraints and draw from the thread's or object's own generator.
Compare std::randomize(len) with { len inside {[1:16]}; } with putting len in a class and calling randomize(). L1
std::randomize() randomizes ordinary variables in the current scope, with constraints written at the call. It suits a one-off value in a sequence or a module. A class gives the constraints a name and a home. Tests can extend them, switch them off, freeze fields and reuse them everywhere. Use the scope form for local choices and classes for anything that describes a transaction.
Why does SystemVerilog give every thread and every object its own random number generator? L1L2
So that one part of the testbench cannot disturb another's values. Each thread and object has its own generator, seeded from its parent thread at creation. So randomizing an unrelated object does not shift this object's values. With Verilog's single shared $random stream, adding one call anywhere reshuffled everything after it.
A failing seed stops failing after someone adds one debug object to the environment. Nothing else changed. How? L2
Each object takes its seed from the creating thread, in creation order. A new object created earlier takes the seed that a later object used to get. Every object built after it now draws different values, and the failing stimulus is gone. Seed critical objects explicitly with srandom(), or keep new objects at the end of construction.
Chasing a One-in-500 Failure5
Walk me through how you reproduce and fix a test that fails on about one seed in 500 in the nightly regression. This is what the log gives you. L3L4
First rerun that seed with the same build, test and plusargs. This log shows it fails again, so the trigger is in the stimulus. Then find the first divergence between DUT and model, dumping waves only around it, and decide which side is wrong. After the fix, rerun the seed and a fresh batch. Then add a cover point and a directed sequence for the corner. The next code change will move the random stream away from it.
QuestionPractise Chasing a One-in-500 Failure →Why can the failing seed stop failing after an unrelated testbench change, before any fix? L2L3
A seed fixes the root generator, and every thread and object derives its own seed from its parent in creation order. Add a component, reorder construction or change a loop count, and later objects get different seeds and different stimulus. The bug is still there; the seed just no longer reaches it.
Follow-upPractise Why a Seed Stops Reproducing After a Change →What if the same seed on the same build passes when you rerun it? L3
Then something besides the seed decides the outcome. Look first for races between testbench and DUT, which can flip with process order. Then check uninitialised values, code that depends on $time or the environment, and the rerun command. Running the seed on a second simulator or with different process ordering often makes a race show itself.
Once the bug is fixed, do you add the failing seed to the regression, or write a directed test for the corner? Compare them. L3
A saved seed is cheap but brittle: the next testbench change moves its stimulus, and it quietly stops testing the corner. A directed test, or a constrained sequence aimed at the corner, keeps hitting it whatever else changes. Add a cover point for the corner as well, so the regression report shows whether random tests reach it on their own.
Follow-upPractise Keep the Random Seed or Write a Directed Test →At subsystem level the nightly run shows 300 failing seeds. How do you organise triage so the team works on causes, not symptoms? L4
Bucket failures automatically by first error signature, with addresses and IDs stripped, and pick the shortest reproducing seed per bucket. Assign each bucket an owner and track it to a cause, because one bug often produces dozens of signatures downstream. Rerun failing seeds automatically to separate reproducible failures from flaky ones, and treat the flaky list as bugs to fix, not noise.
Follow-upPractise Triage When the Nightly Shows Hundreds of Failures →The Regions of a Time Slot5
Walk through the regions of one simulation time slot. What runs in each, and why should a testbench writer care? L1L2
Preponed samples values for assertions and clocking blocks before anything changes. Active runs process code and blocking assignments, Inactive runs code delayed by #0, and NBA applies the <= updates. Observed evaluates assertions, Reactive runs their action blocks and program code, and Postponed runs $strobe and $monitor once nothing can change. Where a line of testbench code runs decides which value it sees.
Compare writing the testbench in a program block with writing it in a module that drives through clocking blocks. L2
A program runs its code in the Reactive region, after the design has settled, so its reads never race with design updates. A clocking block gets the same effect at the signal level. It samples inputs before the edge and drives outputs at a skew after it, from ordinary module or class code. Most class-based benches, UVM included, use modules plus clocking blocks, because programs add restrictions such as no always blocks.
Program blocks were added to SystemVerilog to fix testbench races. Why do most teams no longer use them? L2L3
Clocking blocks already remove the TB-to-DUT race at the signal boundary, and that is where the race actually lives. Programs add awkward rules on top, such as special rules for when the simulation ends. They also mix poorly with class-based code started from modules. The industry settled on modules, classes and clocking blocks.
Follow-upPractise Why Program Blocks Fell Out of Use →What if an assertion's fail action prints the signals it checked? Which values appear? L2
The assertion decided using Preponed samples, but its action block runs later, in Reactive, after the NBA updates. Printing a signal there shows its new value, which can contradict the failure. Print $sampled(sig) to show what the assertion actually saw.
How can two always blocks stop simulation time from ever advancing? L1L2
Suppose each block wakes on a change that the other makes, with no delay between. The two blocks keep creating new events in the same time slot, so time never moves on. An accidental combinational loop such as assign a = ~a; does it too. Simulators detect it with an iteration limit or appear to hang.
The Monitor That Waited Zero Delays3
A monitor wakes on posedge clk, waits #0 to "let the design settle", then reads the DUT's output flop. The log shows every beat one cycle late. A colleague suggests a second #0. What is going on? L1L2
The flop is written with <=, so its new value lands in the NBA region. A #0 only moves the monitor to the Inactive region, which runs before NBA. No number of #0 delays gets past it, because NBA starts only when Active and Inactive are both empty. Instead, sample the flop through a clocking block at the next edge. There, the value written at this edge is settled.
Why is the NBA region placed after the Inactive region instead of before it? L2
Every process that wakes at the edge must read pre-edge values, even one that waits with #0. Only then do nonblocking assignments model flops correctly. If NBA updates landed before Inactive, a #0 reader would see half-updated state. Then the result of <= would depend on process order again.
What if the monitor waits with @(q) after the edge instead of using #0? L1L2
It wakes on the NBA update and reads the new value, but only when the value actually changes. A beat that repeats the previous value never wakes it, so the monitor silently drops it. Sampling once per clocking event is the only scheme that sees every beat.
Follow-upPractise A Monitor That Waits for the Signal to Change →Display, Strobe and Monitor at the Same Edge5
What does this print? L0L1
At t=5, $display prints q=0, because it runs before the NBA update, while $strobe prints q=1 at the end of the slot. $monitor prints at t=0, t=5 and t=15, each time q has changed by the end of the slot. Use $display for "now", $strobe for the settled value, and $monitor for a change log.
Why may nothing change a value in the Postponed region? L1
Postponed exists to report the final values of a time slot. If code there could write a variable, the slot would no longer be final. Then $strobe or $monitor could print values that differ from the real final values. So Postponed is for reading and reporting only.
Break it: a value changes through a #0 assignment at the edge. Does $strobe print the new value? L1L2
Yes. A #0 delay moves the write to the Inactive region of the same time step. Postponed comes after Inactive and NBA, so $strobe prints the value after that write. A $display that ran earlier in the Active region printed the old value.
Compare $monitor and $strobe as logging tools. What does this print? L1L2
It prints only monitor B lines: b=0 at t=0 and b=1 at t=20, because a new $monitor call replaces the previous one. $strobe prints once per call, so it suits a log written from a clocked loop, while $monitor suits one quick change log. That one-at-a-time rule is why benches log through their own monitors instead.
What if the $display sits inside the clocked block itself, right after q <= q + 1;? L1
It still prints the old q. The <= only scheduled the update, so the next statement in the same block reads the pre-edge value. That is useful in RTL debug: the printed value is what the flop held during the cycle that just ended.
The Monitor That Copied to an Int6
A read monitor copies the DUT's rdata into an int before comparing it with the expected value. The DUT never reset the upper nibble. What does this print, and what has the monitor hidden? L0L1
It prints rdata=xxxx0101 seen=5. The copy into the two-state int turns every x and z bit into 0. So the copy looks like a clean 5, and any check after it passes. Keep DUT-facing data in four-state types such as logic, and test it with $isunknown before any conversion to a two-state type.
If four-state types are safer, why does SystemVerilog have two-state types at all? L1
A two-state bit needs half the storage and simpler arithmetic, which matters for large arrays, scoreboards and counters. Two-state types also match C, so they map cleanly across DPI, and random variables are generated as two-state values. Testbench-internal data rarely needs x, so it is the natural home for them.
Follow-upPractise Why Two-State Types Exist →Compare $isunknown(r), r === 'x and r == 'x as checks for an unknown bus. L1
$isunknown is true when any bit is x or z, which is the check you want. === compares x and z bits literally, so r === 'x is true only when every bit is x and misses a half-driven bus. == returns x whenever an operand has an x bit, so it never gives a clean true. The run shows all three on xxxx0101.
You add assert property (@(posedge clk) !$isunknown(rdata)); and it fails on almost every cycle of a passing test. What is wrong with the check? L1L2
The bus is allowed to be unknown when nothing is being transferred; only data that rvalid marks as valid must be known. Write rvalid |-> !$isunknown(rdata), and check the qualifier itself with a separate unconditional !$isunknown(rvalid) after reset.
What if the DUT releases the bus (drives z) instead of leaving it x? L1
The result is the same: z also converts to 0 in a two-state variable, so an undriven bus reads as zeros. $isunknown treats z like x, so the same check before the copy catches both.
At chip level much of the environment is two-state, and the emulation runs are two-state too. How do you stop X bugs from escaping? L3
Draw the line at the DUT boundary: interfaces and monitors stay four-state, and every bus gets a valid |-> !$isunknown(data) assertion bound to it. Reset and power-up scenarios run on a four-state simulator as a sign-off item. Emulation normally runs two-state; X modes exist but cost speed, so reset-state X checks belong in four-state simulation. Two-state is allowed only behind that boundary, where speed matters and nothing samples the DUT.
Is Logic a Wire or a Reg?6
Is logic a wire or a reg? When do you still have to write wire? L0L1
logic is a data type, not a kind of object. Declared alone in a module, it makes a variable. wire logic makes a net. Many procedural statements may write a variable, but only one continuous driver may drive it. A net combines (resolves) several drivers. So write wire for multiply driven or bidirectional signals, such as a tri-state bus or an inout, and logic for the rest.
Why does the language let a net have many drivers but a variable only one continuous driver? L1
A variable simply holds the last value written to it; it has no rule for combining two simultaneous drivers. A net is modelled as a wire with a resolution function that merges drivers by value and strength. Forbidding a second continuous driver on a variable turns an accidental short into a compile error instead of a silent last-writer-wins race.
Follow-upPractise Why a Variable Allows One Continuous Driver →What if two always_ff blocks both write the same logic variable? L1L2
That is two procedural writers, which plain always blocks would allow. always_ff adds a rule: a variable it writes must not be written by any other process. So the tool reports an error. With plain always blocks, the last write in each time step would win a race.
A port is declared input logic [7:0] d. Is d a net or a variable inside the module? And output logic [7:0] q? L2
When the kind is left out, an input port with a data type becomes a net. So d is a wire of type logic. An output port with a data type defaults to a variable, so q can be written procedurally but by only one driver. That is why output logic catches two blocks driving the same output, while inputs still resolve like wires.
A block's output is stuck at 0000000z although the testbench drives data_in correctly. The instance connects .d(dta_in). Why did it compile, and how do you stop it happening again? L1L2
An undeclared name in a port connection silently creates an implicit one-bit net, so dta_in is a new floating wire. Put ` default_nettype none ` at the top of every file, which turns implicit nets into compile errors, and restore default_nettype wire `` at the end if legacy code needs it.
You want to catch a second driver on an internal signal. Compare declaring it as uwire with declaring it as a logic variable. L2
Both make a second continuous driver an error. A uwire is still a net, so it can connect module ports like any wire. But procedural code cannot write it. A logic variable can also be written from a procedural block, which is often what an RTL signal needs.
Which Byte Is Index Zero?5
What does this print? Then tell me when you would choose a packed array and when an unpacked one. L0L1
It prints p[0]=dd p[3]=aa $bits(p)=32. Packed dimensions form one contiguous vector, and with [3:0] index 0 is the least significant byte. An unpacked array such as u[4] is four separate elements: it cannot take a 32-bit literal or join arithmetic without streaming. Pack fields that travel as one bus or word; unpack collections you index like a memory.
Why can't you just add or shift an unpacked array the way you can a packed one? L1
The language guarantees no bit layout for unpacked elements: a simulator may store each element in its own word, with padding. Whole-value operators need one contiguous bit string, which only packed dimensions promise. Unpacked arrays support element-wise copy and equality instead.
Follow-upPractise Why an Unpacked Array Is Not a Vector →What if you cast an unpacked byte array to a packed word, and the sizes differ by one byte? L1L2
A bit-stream cast needs both types to have exactly the same number of bits. With fixed-size types, a mismatch is a compile error. With a dynamic array or a queue, the cast fails at run time. So size the target from the source, and check the size before the cast.
Follow-upPractise Bit-Stream Casts of Unpacked Arrays →How do you turn the unpacked u[4] into a 32-bit word, and how do you then reverse its byte order? Predict the output of this code. L1L2
It prints v=aabbccdd w=ddccbbaa. {>>{u}} streams the elements left to right, so element 0 lands in the most significant byte. {<<8{v}} reverses the order of 8-bit slices. A concatenation {u[0], u[1], u[2], u[3]} gives the same v but has to name every element, so it breaks when the size changes.
Your block has two hundred control registers with bit fields. How would you model them in the testbench so field mistakes are caught early? L2L3
Describe each register as a packed struct with named fields. Then reg.mode replaces unnamed slices like r[6:4], and a width mismatch fails at compile time. Generate those types and the address map from the same specification the RTL uses, rather than writing them by hand. For a large map, the usual choice is a generated UVM register model. It adds access policies and a mirror of each register on top of the fields.
The Negative Number That Was Not Smaller5
What does this print, and why? L0L1
It prints surprise. An expression is signed only if every operand is signed. So the unsigned u makes the whole comparison unsigned, and s is read as 255. Declaring s as signed does not help. Make both operands signed, widening the unsigned one first with $signed({1'b0, u}) so that a large value cannot flip negative.
Why did the language designers make signedness depend only on the operands, not on where the result goes? L1L2
The standard evaluates an expression in two steps. First, the operands fix the width and signedness of each part. Then the operands are extended to that size. Keeping the result target out of the first pass means one expression has one meaning wherever it appears. The price is the trap you just saw, so the rule is to make operands agree.
Follow-upPractise Why the Operands Decide Signedness →What if you write a < -1 with a declared logic [7:0]? L1L2
One unsigned operand makes the whole comparison unsigned. Both sides are sized to 32 bits, the width of the literal. So -1 becomes 32'hFFFF_FFFF, and a < -1 is true for every 8-bit a. Make the unsigned side signed and wide enough, as in int'(a) < -1.
Compare fixing the comparison with $signed(u) and with $signed({1'b0, u}). When do they differ? L1L2
$signed(u) reinterprets the same 8 bits, so any value of 128 or more becomes negative: 200 turns into -56. $signed({1'b0, u}) first adds a zero sign bit, so 200 stays 200 as a 9-bit signed value. They agree for small values, which is why the plain cast passes a quick test and fails in the field.
A test hangs in this loop. What is wrong? L1
An unsigned counter is never below zero, so i >= 0 is always true; after 0 it wraps to 15 and keeps going. Use a signed loop variable such as int, or count up.
Putting an Integer in an Enum5
You read an opcode from a register as an int and need it in an op_t enum variable. How do you assign it, and what can go wrong? L0L1
A plain assignment from int to an enum is a compile error, so you must cast. A static cast op_t'(raw) is unchecked: it stores whatever value arrives, even one with no name. $cast(op, raw) checks that the value is a member, returns 0 if not and leaves op unchanged. Use $cast with its return value tested whenever the value comes from the DUT.
What does this print? The enum's members are 1, 2 and 4. L1L2
It prints default s=0 name="". An unassigned enum variable takes the default of its base type, here 0 for int, which is not one of the members. A decoder that expects a member then meets a value with no name. Give the variable an explicit initial member, or reset it.
Compare a packed struct, a packed union and a tagged union. The bus word is either a read command or a write command. L1L2
A packed struct lays every field side by side, so it suits a header whose fields are all present. A packed union gives several views of the same bits. You can write through the read view and read through the write view, with no check. A tagged union also stores which member is valid. Reading the wrong member is an error, so it suits a command that is one kind or the other.
Follow-upPractise Packed Structs, Packed Unions and Tagged Unions →Why bother with an enum instead of localparam constants for opcodes? L0
An enum is a type: assignments from unrelated integers need a cast, so a mixed-up value fails to compile. Its values carry names, so $display with %s and name(), and most waveform viewers, show WRITE instead of 2. Iteration methods such as first() and next() also let a test walk every legal value.
What if a test must try every opcode? How do you loop over the enum? L0L1
Start at first(), step with next(), and stop after last(). This covers every member, even after someone adds a new opcode. A loop over the integers 0 to N would hit gaps or miss new values. num() gives the member count, which a check can use.
The Unknown Select That Picked a Side5
The select comes out of reset as x. What does this print, and what would real hardware do? L1L2
It prints y=0. An if treats an x condition as false and takes the else branch. So the unknown select gives a clean value, and the x disappears. Real hardware would pick one input or the other, possibly a different one on each chip. This X-optimism can hide a missing reset, so check the select with $isunknown or an assertion instead of trusting the output.
Compare if (sel) y = a; else y = b; with y = sel ? a : b; when sel is x. L1L2
The if takes the else branch and gives b. With an x condition, the conditional operator evaluates both sides and merges them bit by bit. Equal bits stay, and differing bits become x. So sel ? 1 : 0 gives x, and sel ? 4'b1100 : 4'b1010 gives 1xx0. The run shows all three.
Why does the language pick the else branch for an x condition instead of making everything unknown? L1
A procedural if must run one branch or the other. It cannot run both branches and merge what their statements did. The rule picks "not true", which is cheap and deterministic but optimistic. The conditional operator can merge because it produces a value, not side effects.
Compare if with case for an x select. Which branch does the case take? L1L2
case compares with exact matching, so an x select matches only an item that has x in the same bits. Normally no item does, so the default runs, or nothing runs. Like if, it picks one path and hides the x. casex treats x bits as wildcards, so it can hide the x even more.
How do you sign off that a block has no X bugs, given that RTL simulation is optimistic? L3
Combine three things. Assertions check that every valid output and every state register is known a few cycles after reset. A reset test runs on a four-state simulator with no forced initial values. Gate-level runs cover the reset paths, and their pessimism shows what RTL hid. Review every waiver, because a pessimistic false x and a real one look the same.
Follow-upPractise Reset and X Sign-Off for a Block →Your Review Queue
Saved review reminders are coming soon.