This page is for anyone facing a VLSI round, whether you are aiming for RTL design, design verification or physical design. Most interviews open with digital basics like setup and hold, metastability and resets, then move to RTL coding traps and clock domain crossing. From there they test static timing, synthesis constraints, testbench and coverage thinking, and the physical design flow from floorplan to routing, with a low-power question or two. Senior rounds add a timing closure story and a judgement call near tapeout. Each question shows what the interviewer is checking, the shape of a strong answer and a short answer to say out loud.
Search all questions by round, difficulty and level, or save the ones you want to practise.
Setup: the minimum time data must be stable before the active clock edge.
Hold: the minimum time data must stay stable after the edge.
Violation: the flop may capture the wrong value or go metastable.
Fixing: setup depends on the clock period; hold does not, so it must be fixed with delay.
"A flip-flop needs the data at its input to be steady for a short window around the clock edge. Setup time is how long before the edge the data must already be stable, and hold time is how long after the edge it must stay stable. If data changes inside that window, the flop can capture the wrong value, or worse, go metastable and sit at an in-between level for a while before settling. In practice the two behave very differently. A setup violation means the path is too slow for the clock, so I can speed up the logic or, as a last resort, slow the clock down. A hold violation means new data raced through too fast and overwrote the old value, and changing the clock frequency doesn't help at all, so it has to be fixed by adding delay on the data path."
Saying a hold violation can be fixed by lowering the clock frequency.
Cause: an input changes inside the setup and hold window, so the flop output hovers before resolving.
Synchronizer: the second flop gives the first a full clock cycle to settle before anyone uses its output.
MTBF: improves exponentially with resolution time and falls as clock and data rates rise.
Rules: no logic between the two flops, keep them close, add a third stage at very high speed.
"Metastability happens when a flop's input changes right inside its setup and hold window, usually because the signal comes from another clock domain. The output can hang at an in-between voltage and take an unpredictable time to fall to a clean zero or one. You can't eliminate it, you can only make failures rare. A two-flop synchronizer does that by letting the first flop go metastable if it wants, and giving it almost a whole clock period to resolve before the second flop samples it. The mean time between failures grows exponentially with that resolution time, and it drops as the clock frequency and the data toggle rate go up. So I keep no logic between the two flops, ask for them to be placed close together, and at high clock speeds I'll use three stages instead of two."
Claiming a synchronizer guarantees the output is never metastable, or that it also fixes multi-bit buses.
Synchronous: sampled on the clock edge like data; needs a running clock and sits in the data path.
Asynchronous: acts immediately with no clock, but release near a clock edge breaks recovery and removal.
Synchronizer: two flops with async reset, first input tied high, so reset goes in at once and comes out on a clock edge.
"A synchronous reset is only seen on a clock edge, so it behaves like any other data input. That's clean for timing, but the clock must be running for reset to work, and the reset logic sits in front of every flop's data pin. An asynchronous reset takes effect straight away without a clock, which is great at power-up. The danger is when it's released. If reset goes away too close to a clock edge, flops can violate recovery or removal time, go metastable, or leave reset on different cycles, and a state machine can wake up in an illegal state. So the usual answer is a reset synchronizer: reset goes into every flop asynchronously, but the release passes through two flops in the destination clock domain, so it always comes out lined up with that clock. STA then checks recovery and removal on it like any other path."
// reset synchronizer: async assert, sync de-assert
always @(posedge clk or negedge arst_n)
if (!arst_n) {r1, r2} <= 2'b00;
else {r1, r2} <= {1'b1, r1};
assign rst_n_sync = r2;
Saying asynchronous resets are always safe because they don't depend on the clock, ignoring what happens at release.
Size: pick core area and target utilization, leave room to grow.
Macros: follow the data flow, push them toward the edges, pins facing the logic that uses them.
Room: channels between macros wide enough to route, halos and blockages around them.
Check: trial placement for congestion, early timing on macro paths, power grid reaching every macro.
"I start from the data flow. I look at which logic talks to each memory and where the block's inputs and outputs sit, then place the macros so data moves in a sensible direction instead of criss-crossing the die. Usually I push macros toward the edges and keep the middle open for standard cells, and I orient them so their pins face the logic they connect to. Between macros I leave channels wide enough for the wires that must pass through, and I add halos so cells don't crowd the macro pins. I avoid odd notches and narrow gaps where cells get trapped. Then I build the power grid and make sure every macro is hooked into it properly. Before calling it done, I run a quick trial placement and look at the congestion map and early timing on paths into and out of the memories. A bad floorplan usually shows up there first."
Placing macros by fitting them in wherever there is room, with no thought for data flow or routing channels.
Inputs: netlist, constraints, timing libraries, physical cell views and technology rules.
Build: floorplan and power grid, placement, clock tree synthesis, routing.
Optimize: timing optimization after placement, after CTS and after routing.
Sign-off: STA with extracted parasitics, DRC, LVS, IR drop and electromigration, then GDS.
"It starts with the netlist from synthesis, the timing constraints, the timing libraries, the physical views of the cells and macros, and the technology rules. Floorplanning comes first: die and core size, where the macros and IO go, and the power grid. Then placement puts every standard cell in a legal spot, trying to keep timing-critical cells close and avoid congestion, with an optimization pass after. Next is clock tree synthesis, which builds a real buffered clock network to every flop with low skew, and after that hold fixing starts since the clocks are now real. Routing connects everything with actual metal, first globally then in detail, and there's another round of optimization and signal integrity fixes. Finally sign-off: timing with extracted parasitics across all corners, physical checks like DRC and LVS, and power checks like IR drop and electromigration. When all are clean, the layout goes out as GDS."
Putting CTS before placement, or skipping the sign-off checks entirely.
Goal: deliver the clock to every sink with low skew and reasonable insertion delay.
Quality: skew, latency, transition times, and clock power.
Special handling: clock buffers and inverters, wider spacing or width rules, shielding on key nets.
After: propagated clocks, hold fixing, and useful skew if needed.
"Before CTS the clock is treated as ideal, arriving everywhere at the same moment. CTS builds the real network of buffers and wires from the clock source to every flop and memory clock pin. The main goals are low skew between sinks that talk to each other, an insertion delay that isn't excessive, and clean transition times so flops see sharp edges. It also has to watch power, because the clock tree switches every cycle and is one of the biggest consumers of dynamic power. To judge a tree I look at the skew and latency reports per clock, the worst transitions, the number of buffers and clock power, and then the timing after switching to propagated clocks, especially hold. Clock nets usually get special cells with balanced rise and fall, and routing rules like extra width or spacing, sometimes shielding, to reduce crosstalk and variation."
Saying the only goal of CTS is zero skew, ignoring latency, transition and power.
Blocking: updates immediately, so the next line sees the new value.
Non-blocking: reads all right-hand sides first, updates at the end of the time step.
Rule: non-blocking in clocked blocks, blocking in combinational blocks, never mixed on one variable.
"A blocking assignment, the plain equals, updates the variable right away, so the next statement in the block sees the new value. A non-blocking assignment, less-than-equals, evaluates the right-hand side now but updates the variable at the end of the time step, after every block triggered by that clock edge has read the old values. That matches how real flops behave: they all sample at the same instant. So my rule is non-blocking in clocked always blocks and blocking in combinational ones. If I use blocking in a shift register, like b equals a then c equals b, c gets a in the same cycle and I lose a pipeline stage in simulation. Worse, if two clocked blocks read each other's outputs with blocking assignments, the result depends on which block the simulator runs first, which is a race that may not match the hardware."
always @(posedge clk) begin
b <= a; // both read old values
c <= b; // c gets the old b: two real stages
end
Saying the two are interchangeable, or that non-blocking means the statements run in parallel threads.
Cause: some output is not assigned on every path through the block, so it must remember a value.
Typical code: an if with no else, or a case with no default and missing items.
Fix: default assignments at the top of the block, full if-else or case coverage, use always_comb.
"A latch shows up when a combinational block doesn't assign an output on every path. If I write an if with no else, or a case that doesn't cover every value and has no default, then for some inputs the output isn't given a new value, so the hardware has to hold the old one. Holding a value without a clock means a latch. The cleanest fix is to give every output a default value at the top of the block and then override it in the branches. Adding a final else or a default in the case works too. In SystemVerilog I write these blocks as always_comb, because tools will warn when the logic isn't truly combinational. Unintended latches matter because they make timing analysis harder, can cause glitches, and often mean the logic isn't doing what I intended anyway."
Saying you'd just ignore the warning because simulation passes.
Synchronize: if the input is asynchronous, pass it through two flops first.
Delay: keep one more registered copy of the synchronized signal.
Detect: pulse equals current and not previous, high for exactly one cycle.
"First I ask where the input comes from. If it's from another clock domain or a pin, I run it through a two-flop synchronizer so I'm working with a clean signal. Then I keep one more registered copy, the previous value. A rising edge is simply the current value high while the previous value was low, so the pulse is the synchronized signal AND NOT the delayed one. That's high for exactly one clock cycle, because on the next cycle the delayed copy catches up. I use non-blocking assignments in the clocked block and reset everything low. One thing to know: if the input is already high when reset lifts, this gives one pulse, because it looks like an edge. If that's unwanted, I'd hold the output off for the first few cycles. For a falling edge I'd just flip it to the delayed value AND NOT the current one."
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
s1 <= 1'b0; s2 <= 1'b0; prev <= 1'b0;
end else begin
s1 <= din; // two-flop synchronizer
s2 <= s1;
prev <= s2; // one cycle older copy
end
end
assign rise_pulse = s2 & ~prev;
Using the raw asynchronous input directly in the edge logic, or clocking a flop with the input signal itself.
Problem: each bit resolves independently, so the receiver can see a mix of old and new bits.
Gray code: fine when the value only ever changes by one step, like a counter.
Qualifier: hold the data stable and synchronize one control signal that says it is valid.
Streams: use a handshake or an asynchronous FIFO.
"Each bit's synchronizer resolves on its own. If the bus changes from one value to another right near the receiving clock edge, some bits might be captured new and some old, maybe a cycle apart, so for a cycle the receiver sees a value that was never actually sent. The fix depends on the data. If the value only ever moves by one step, like a counter, I can convert it to Gray code so only one bit changes at a time, then synchronize that. For general data, I hold the bus stable in the source domain and send one control bit, like a valid or request toggle, through a two-flop synchronizer. When the receiver sees that control bit, the data has been stable for a while, so it can be sampled safely. If data is streaming continuously, I use a request-acknowledge handshake or an asynchronous FIFO."
Saying more synchronizer stages on each bit will keep the bus coherent.
Pointers: write pointer lives in the write clock, read pointer in the read clock, each one bit wider than the address.
Gray code: one bit changes per increment, so a synchronized pointer is either the old or the new value.
Flags: empty compares read pointer with synchronized write pointer; full compares write pointer with synchronized read pointer, top two Gray bits inverted.
Pessimism: a stale pointer can only keep full or empty on a little longer than needed, which is safe.
"The write side keeps a write pointer in its clock and the read side keeps a read pointer in its clock. Each is one bit wider than the address, so I can tell a full FIFO from an empty one when the address bits match. To make the flags, each side needs the other's pointer, so the pointer is converted to Gray code and sent through a two-flop synchronizer. Gray code matters because only one bit changes per increment. If the receiver samples mid-change, it gets either the old pointer or the new one, never a random value. Empty is generated in the read domain when the read pointer equals the synchronized write pointer. Full is generated in the write domain when the top two Gray bits of the write pointer are inverted compared to the synchronized read pointer and the rest match. Because the synchronized pointer is a couple of cycles old, the flags are pessimistic, never wrong in a dangerous direction."
Saying the pointers are Gray coded to save power, or that stale pointers could let the FIFO overflow.
Symptom: what failed and why it was hard to reproduce.
Root cause: the exact crossing and why it broke.
Fix and prevention: the structural fix and how CDC checks were tightened.
"In my final-year project we had an FPGA design where a status register read back wrong maybe once in a few thousand reads, and never in simulation. Because it was rare and random, I suspected a crossing. The status value was updated in a fast sensor clock domain and read by the processor interface on a slower clock, and someone had put a two-flop synchronizer on each bit of the bus. Occasionally the reader caught half the bits from the old value and half from the new. I changed it so the sensor side held the value in a register and toggled a single flag, which I synchronized, and the reader only captured the bus after seeing the flag change. The errors stopped. The lesson for me was that normal simulation almost never shows these, so I now run a CDC check on every design and treat any multi-bit crossing without a qualifier as a bug until proven otherwise."
Describing a CDC bug but explaining the fix as adding more synchronizer flops on every bit.
Understand: what the signal is and when it changes relative to its use.
Verify the claim: find what guarantees it only changes while the receiver is idle or in reset.
Decide: waive with a documented reason, or add a synchronizer or qualifier if the guarantee is weak.
Protect: an assertion or software rule so the assumption stays true.
"Quasi-static waivers are common and often fine, like a configuration register written once at boot before the receiving logic is enabled. But I don't accept it on words alone. I'd ask what exactly guarantees it's static. Is it only written while the destination block is in reset or has its clock gated? Is that enforced by hardware, or is it only a note in the programming guide that software might ignore? If there's a real mechanism, I'd write the waiver with that reason, and ideally add an assertion that flags any change to the signal while the receiver is active, so simulation catches a violation of the assumption. If the only guarantee is that software probably won't touch it, I'd push for a cheap synchronizer or an enable qualifier instead, because the cost of a flop is tiny next to a silicon bug that shows up once in a million boots."
Waiving it because the designer is senior and sure, without asking what guarantees the signal is stable.
What: checks every path against the constraints without any test vectors.
Paths: input to register, register to register, register to output, input to output.
Points: start at input ports or clock pins of flops, end at flop data pins or output ports.
Versus simulation: simulation only checks what the vectors exercise; STA does not check function.
"Static timing analysis computes the delay of every timing path in the design and checks it against the clocks and constraints, without running any stimulus. A path starts at an input port or the clock pin of a flop and ends at the data pin of a flop or an output port, so you get four kinds: input to register, register to register, register to output, and pure combinational input to output. For each one it checks setup and hold, plus things like recovery and removal on async pins, and design rules like max transition and capacitance. The big advantage over gate-level simulation is coverage. Simulation only proves timing for the paths my vectors happen to toggle, and it's slow on a full chip. STA covers everything, in every corner, much faster. What it doesn't do is check function, which is still simulation's job."
Saying STA needs test vectors or that it verifies the logic is functionally correct.
Arrival: launch clock latency plus clock-to-Q plus logic delay.
Required: period plus capture latency minus setup minus uncertainty.
Slack: required minus arrival; positive means the path meets setup.
Skew: a later capture clock helps setup and hurts hold.
"I work out when the data arrives and when it's required. Data arrival is the launch clock latency, 0.3, plus clock-to-Q, 0.15, plus the logic, 1.4, which gives 1.85 nanoseconds. The required time is the next capture edge, so the 2 nanosecond period plus the capture latency of 0.4, which is 2.4, minus the setup time of 0.1 and the uncertainty of 0.1, giving 2.2. Slack is required minus arrival, so 2.2 minus 1.85, which is 0.35 nanoseconds of positive slack, and the path meets setup. One thing worth pointing out is that the capture clock arrives 0.1 later than the launch clock. That positive skew gave setup an extra 0.1, but the same skew makes the hold check on this path tighter by 0.1, so I'd look at hold too."
Subtracting the capture clock latency instead of adding it, or forgetting uncertainty entirely.
Multicycle: data is only needed every N cycles, for example a register loaded on an enable every second cycle.
Hold shift: moving setup to edge 2 moves the default hold check to edge 1, which is too strict; a hold multicycle of 1 moves it back.
False path: a path that never matters functionally, such as static configuration or clocks that are truly asynchronous.
Risk: a wrong exception hides a real violation, so each one needs review.
"I use a multicycle path when the design guarantees the data only has to be captured every few cycles, say the destination is enabled every second clock. Setting a setup multicycle of 2 tells STA to check setup against the second capture edge instead of the first. The catch is that by default the hold check moves with it, to one edge before the new setup edge, so the tool now demands hold against edge 1. That's far stricter than the real design needs and it'll try to add lots of delay. So I add a hold multicycle of 1 to pull the hold check back to the original launch edge. A false path is different: it removes a path from timing altogether, for things like quasi-static configuration registers or crossings between asynchronous clocks, where the synchronizer takes care of it. Both are powerful, so every exception needs a design reason and a review, because a wrong one hides a real bug."
set_multicycle_path 2 -setup -from [get_cells u_acc/acc_reg*] -to [get_cells u_acc/sum_reg*]
set_multicycle_path 1 -hold -from [get_cells u_acc/acc_reg*] -to [get_cells u_acc/sum_reg*]
set_clock_groups -asynchronous -group [get_clocks clk_a] -group [get_clocks clk_b]
Adding a false path to make a violation disappear without a functional reason.
Variation: the same cell can be faster or slower in different spots due to process, voltage drop and temperature.
Derates: for setup, launch clock and data are made late and capture clock early; hold is the opposite.
Pessimism: the shared part of the clock tree cannot be fast and slow at once.
CPPR: adds back the difference on the common clock segment.
"Corners model the whole chip being slow or fast, but even within one die, two identical cells can have slightly different delay because of local process variation, voltage drop and temperature. On-chip variation handles that by applying derates. For a setup check, STA makes the launch clock path and the data path a bit slower, and the capture clock path a bit faster, which is the worst case for setup. For hold it flips that around. The problem is the part of the clock tree that both the launch and capture flops share. The same physical buffers can't be slow for launch and fast for capture at the same moment, so that difference is pure pessimism. Clock path pessimism removal finds the common point where the two clock paths split and adds back the difference on the shared part. Newer flows go further, with variation that depends on path depth or is modelled statistically rather than one flat derate."
Confusing OCV with process corners, or thinking CPPR adds margin rather than removing false pessimism.
Is it real: check the constraints, clocks and exceptions before touching the design.
Where is the delay: cell delay, long wires, high fanout, poor transitions or bad skew.
Local fixes: upsize, lower threshold voltage cells, buffer or split fanout, pull cells closer, restructure logic.
Bigger fixes: useful skew if the next stage has margin, or pipelining and retiming in RTL; then recheck hold.
"First I make sure the violation is real. I open the path report and check the clocks, the input and output delays and whether this should have been a multicycle path. If it's real, I look at where the delay goes. If it's a few big wire delays, the cells are probably spread apart, so I'd look at placement, maybe a bound to pull them together, or buffer a long net. If there's a high-fanout net with slow transitions, I'd buffer or clone the driver. If it's many levels of logic, I'd upsize the weak cells and swap some to lower threshold voltage versions, which are faster but leak more. If the next stage has spare slack, useful skew can delay the capture clock a little. Two hundred picoseconds is a lot, though. If the path simply has too many logic levels for the clock, I'd go back to the RTL team about pipelining or retiming. After any fix I recheck hold and nearby paths."
Jumping straight to changing the clock, or applying fixes without first reading where the delay actually is.
Context: block, target frequency and how far off it was.
Diagnosis: the pattern in the failing paths, not just the worst one.
Actions: what you tried, what didn't work, what did, and the trade-off.
Result: closed with what margin, and what you'd do earlier next time.
"At my last company I owned a block with a wide arithmetic datapath that was failing setup by a large margin across hundreds of paths after placement. I first grouped the violations instead of chasing the worst one, and most of them went through the same multiplier into a mux feeding a register far across the block. The macros had been placed so that data had to cross the whole area. Upsizing and lower threshold voltage cells recovered some slack but pushed leakage up and still didn't close. So I moved the two memories to shorten the route and added a placement region to keep that logic together, which fixed most of the wire delay. The remaining paths had too many logic levels, so I worked with the RTL designer to add one pipeline stage, which the architecture could tolerate. It closed with a small positive margin. Next time I'd run an early trial placement before freezing the floorplan."
A story where the only action was rerunning the tool with more effort until it passed.
Explain the risk: hold does not depend on the clock period; a real failure breaks the part at any speed.
Check it is real: constraints, corner, extraction, whether the path is actually functional.
Fix small: a delay cell near the capture flop through a quick ECO, then rerun timing in every corner.
Record: if anything is waived, a written reason signed off by the right people.
"I'd push back on ignoring it, calmly and with the reason. Setup being fine doesn't help, because a hold violation doesn't depend on the clock period. If it's real and the silicon lands near that corner, the chip fails at any speed, and we can't slow the clock to save it. So first I'd confirm it's real: right corner, right parasitics, and that the path isn't a genuine false path someone forgot to constrain. If it's real, the fix is usually small, a delay cell or two near the capture flop, placed in spare space. That's a quick ECO. Then I'd rerun full timing in every corner, plus the physical checks, to be sure the fix didn't break setup or create a new issue. If after that there's still a reason to waive, it goes in writing with the path and the justification, approved by the timing and design leads, not decided in a hallway."
Agreeing to ignore it because the violation is small or because setup has margin.
Switches: header or footer power switches, turned on in a chain to limit the current rush.
Isolation: clamps on the block's outputs so live logic never sees floating values.
Retention: flops with an always-on save latch for state that must survive.
Sequence: clock off, save, isolate, power off; back up in the reverse order.
"Power gating puts switch cells between the block and its supply, so the whole block can be cut off and its leakage mostly goes away. That brings extra cells. The outputs of the block need isolation cells, because once power is off those outputs float, and live logic reading them could see garbage or draw short-circuit current. Any state we want to keep goes into retention flops, which have a small latch on an always-on supply. If the block runs at a different voltage from its neighbours, I also need level shifters. Going down, I stop the clock, save state into the retention latches, turn on isolation, then switch power off. Coming back, I turn the switches on gradually so the current rush doesn't disturb nearby logic, wait until the supply is stable, restore the state, release isolation and start the clock. All of this goes in the power intent file, so the tools and the checks know the plan."
Describing power gating as just turning the supply off, with no isolation or sequence.
Dynamic: switching power scales with activity, capacitance, frequency and voltage squared.
Leakage: current that flows even when nothing switches, growing with temperature and lower threshold voltage.
Dynamic fixes: clock gating with proper gating cells, lower voltage and frequency, less activity.
Leakage fixes: high threshold voltage cells off the critical path, power gating with isolation and retention.
"Power splits into dynamic and static. Dynamic is mostly switching power, which goes with how often nodes toggle, the capacitance being charged, the frequency, and the supply voltage squared, so voltage is the strongest knob. Static is leakage, which flows even when nothing switches. For dynamic power, clock gating is the first thing: stop the clock to registers that aren't changing, using an integrated clock gating cell rather than an AND gate, because the cell's latch stops the enable from glitching the clock. Lowering voltage and frequency when full speed isn't needed also helps a lot. For leakage, I use higher threshold voltage cells on paths with spare slack, and power gating to switch off idle blocks completely. Power gating needs isolation cells so a dead block doesn't send garbage to live ones, retention flops if state must survive, and level shifters wherever domains run at different voltages. All of that is described in a power intent file."
Saying clock gating reduces leakage, or forgetting isolation when a block is powered off.
Inputs: RTL, timing libraries for the standard cells, and SDC constraints.
Steps: elaborate, optimize generic logic, map to library cells, optimize for timing, area and power.
Outputs: gate-level netlist, constraints for later stages, timing, area and power reports.
Checks: unintended latches, removed registers, unconstrained paths, timing summary.
"Synthesis takes the RTL, the timing libraries that describe every standard cell's delay and power, and the SDC constraints that say how fast the clocks are and what happens at the ports. It first elaborates the design into generic logic, inferring flops, latches, adders and so on, then optimizes that logic and maps it onto real cells from the library, choosing cell types and sizes to meet timing with the least area and power. The output is a gate-level netlist plus reports and constraints for the next stage. Before handing off, I read the log carefully. I look for inferred latches I didn't want, registers that got removed because they had no load or were constant, unconstrained or unclocked paths, and warnings about undriven nets. Then I check the timing summary and area against the target, so problems are found now and not in physical design."
Describing synthesis as just converting Verilog to gates with no mention of constraints or libraries.
Clocks: create_clock on the source with a period; generated clocks for anything derived.
Input delay: how much of the period is already used outside before data reaches the input port.
Output delay: how much of the period the outside logic needs after data leaves the output port.
Margin: clock uncertainty, plus drive and load on the ports.
"I start with create_clock on the clock port with the right period, so the tool knows every edge. If the block divides that clock, I declare it with create_generated_clock so STA knows its relationship to the source instead of treating it as unrelated. Then the ports. set_input_delay says how much of the clock period has already been used by logic outside the block before data arrives at my input, so the internal path only gets what's left. set_output_delay says how much time the logic outside needs after my output, so my internal path has to finish that much earlier. I add clock uncertainty for jitter and margin, plus a driving cell and load on the ports so transitions are realistic. With a 4 nanosecond clock and 1.5 of input delay, the path from that input to the first flop has about 2.5 nanoseconds, minus setup and uncertainty."
create_clock -name core_clk -period 4.0 [get_ports clk]
set_clock_uncertainty 0.15 [get_clocks core_clk]
set_input_delay 1.5 -clock core_clk [get_ports data_in*]
set_output_delay 1.2 -clock core_clk [get_ports data_out*]
create_generated_clock -name clk_div2 -source [get_ports clk] -divide_by 2 [get_pins u_div/q_reg/Q]
Saying input delay is the delay inside the block, or leaving ports unconstrained and calling timing clean.
Code coverage: automatic; line, branch, condition, toggle and FSM coverage of the RTL.
Functional coverage: written from the verification plan; covergroups, crosses and cover properties.
Gaps: code can run without being checked, and missing features have no code to cover.
Sign-off: both kinds, plus passing checkers and reviewed holes.
"Code coverage is collected automatically by the simulator. It tells me which lines, branches, conditions and toggles in the RTL were exercised, and which FSM states and transitions were hit. Functional coverage is something I write myself from the verification plan, using covergroups, coverpoints, crosses and cover properties, to record whether the features and scenarios I care about actually happened, like a full FIFO while a reset arrives. Full code coverage isn't enough because it only says code ran, not that anything checked the result. It also can't measure a feature that was never implemented, since there's no code to miss. And it doesn't see combinations, like two interfaces being busy at the same time. So I sign off on both, with every checker passing and every remaining hole reviewed and either closed or waived with a reason."
Saying full code coverage means the design is fully verified.
Stimulus: sequences create sequence items; the sequencer hands them to the driver.
Pins: the driver drives the interface; the monitor watches it and rebuilds transactions.
Checking: the monitor broadcasts on an analysis port to the scoreboard and coverage.
Structure: agents per interface inside an env, a test on top that configures and picks sequences.
"At the top is the test, which builds the environment, sets up configuration and chooses which sequences to run. The environment holds one agent per interface, plus the scoreboard and coverage collectors. Each agent has a sequencer, a driver and a monitor. A sequence creates transactions, called sequence items, and sends them through the sequencer, which passes them to the driver when it asks for the next one. The driver turns each item into real pin activity through a virtual interface. Separately, the monitor only watches the pins, rebuilds what happened into transactions, and broadcasts them on an analysis port. The scoreboard picks those up, compares them with what a reference model expects, and flags mismatches, while coverage collectors sample the same transactions. The split pays off in reuse: at the chip level I can switch an agent to passive, keep only the monitor, and reuse the checking unchanged."
Having the scoreboard compare against what the driver sent instead of what the monitor observed.
Clocking and reset: sample on the clock, disable during reset.
Window: overlapped implication into a delay range of one to three cycles.
Stability: if req is high without ack, req must still be high next cycle.
Operators: overlapped starts checking the same cycle, non-overlapped the next.
"I'd write two properties, both clocked on the rising edge and disabled while reset is active. The first says: whenever req is high, ack must be seen somewhere between one and three cycles later. That's req, overlapped implication, then a delay range of one to three into ack. The second covers the hold rule: if req is high and ack hasn't come yet, then in the next cycle req must still be high, which uses the non-overlapped implication. One detail I'd mention is that the first property starts a new check on every cycle req is high. If req stays high while waiting, each of those attempts needs its own ack within three cycles, so depending on the protocol I might trigger only on the rising edge of req. I'd also add a cover property so I can see the handshake really happened, because an assertion that never triggers proves nothing."
property p_ack_window;
@(posedge clk) disable iff (!rst_n)
req |-> ##[1:3] ack;
endproperty
a_ack_window: assert property (p_ack_window);
property p_req_held;
@(posedge clk) disable iff (!rst_n)
(req && !ack) |=> req;
endproperty
a_req_held: assert property (p_req_held);
Mixing up overlapped and non-overlapped implication, or forgetting to disable the check during reset.
Class: rand fields plus constraint blocks describe the legal space.
Solve: randomize picks values that satisfy every constraint; inline constraints narrow it per test.
Checks: always check randomize succeeded; use seeds to reproduce a failure.
Why: finds corners nobody thought of; coverage shows what was actually hit.
"I put the fields of a transaction in a class and mark them rand, then write constraints that describe what's legal: aligned addresses, a length range, maybe a weighted mix of reads and writes. When I call randomize, the solver picks values that satisfy all of them, and a test can add inline constraints to steer it, like limiting addresses to one region. I always check the return value, because a conflicting constraint makes randomize fail and otherwise the test quietly runs with old values. Directed tests are great for specific features and known bugs, but they only hit what I thought of. Constrained random explores combinations nobody wrote down, and running with different seeds keeps finding new ones. The catch is I can't know what random did without functional coverage, so the two go together, and I keep the seed of any failing run so I can reproduce it exactly."
class bus_txn;
rand bit [31:0] addr;
rand bit [7:0] len;
rand bit is_write;
constraint c_align { addr[1:0] == 2'b00; }
constraint c_len { len inside {[1:16]}; }
constraint c_mix { is_write dist {1 := 3, 0 := 1}; }
endclass
// in a test
bus_txn t = new();
if (!t.randomize() with { addr < 32'h0000_1000; })
`uvm_error("RAND", "randomize failed")
Not checking whether randomize succeeded, or claiming random tests make coverage unnecessary.
Situation: the block, the failing test and why the designer doubted it.
Evidence: waveform, the exact cycle, the minimal reproducing case.
Outcome: the fix, and the test, assertion or coverage point that keeps it fixed.
"In my last project I was verifying a DMA block and a random test failed with one missing write at the end of a long burst. The designer's first reaction was that the testbench was wrong, because that state machine had been stable for months. Instead of arguing, I narrowed it down. I reran the seed, found the exact cycle, and saw the failure only happened when a new request arrived on the same cycle the burst counter wrapped. I wrote a short directed test that hit just that case and showed him the waveform with the counter and the request side by side. He saw it in a few minutes and fixed the priority between the two conditions. I then added an assertion on that corner and a coverage cross so we'd know it was exercised in every regression. What I took away is that a small, clear repro settles these debates faster than any discussion."
A story where you won the argument but never added a test or check to stop the bug coming back.
Triage: sort holes into unreachable, low-risk and real feature gaps.
Act: targeted tests or constraint changes for the risky holes; exclusions with reasons for the unreachable ones.
Decide together: a written list of what is not covered and its risk, so sign-off is a conscious choice.
"I wouldn't just argue for more time or quietly agree. First I'd go through the holes with the designer and sort them. Some will be bins that can't happen in this configuration, which I'd exclude with a written reason. Some will be low-risk corners. And some will be real features or combinations we haven't exercised, which is what matters. When random has plateaued, pushing more seeds usually doesn't help, so for the risky holes I'd write targeted directed tests or adjust constraints to steer into them, which is often a few days of work. Then I'd give my lead a short list: what's still uncovered, why, and how risky each item is. If we sign off with gaps, it should be a decision we've both made with eyes open and on record, and I'd keep the remaining work going so it's closed for the next revision or an FPGA run."
Waiving every hole to hit the number, or refusing sign-off without offering any triage.
ClapAssist is an AI interview assistant for Mac and Windows. It listens to the interview on your computer and shows you what to say, in short lines you can read while you talk. Your resume and notes are never stored on our servers. It stays out of screen share on every plan; only you can see it.