면접 예상질문

Seungyun Lee·2026년 8월 5일

AXI4_UVM_FULL

목록 보기
16/16

AXI4 UVM — Interview Q&A

DV Intern interview preparation
Speak these out loud until they feel natural. Keep answers ~30-60 seconds each.


How to use this

  • Bold = the one-sentence answer (lead with this)
  • Then 2-3 supporting sentences
  • ⭐ = high priority, almost certain to be asked

Part 1: UVM Architecture

Q1. ⭐ Why are UVM components split into phases? What is the order and direction of build / connect / run?

Phases give every component a synchronized, predictable lifecycle, so construction, connection, and execution never overlap.

build_phase runs top-down — the test builds the env, the env builds the agent, the agent builds the driver and monitor. This order matters because a parent must create and configure a child before the child builds itself. connect_phase runs bottom-up and wires the TLM ports together, for example connecting the driver's seq_item_port to the sequencer's export. run_phase is the only time-consuming phase, where the actual stimulus runs. Splitting these prevents a component from trying to connect to something that doesn't exist yet.


Q2. ⭐ Why do we need config_db? What happens without it?

config_db is how the testbench passes configuration and the virtual interface down the hierarchy without hard-coding dependencies.

The classic use is the virtual interface: tb_top sets it into config_db, and the driver and monitor get it in their build phase. Without config_db, I'd have to manually pass handles through every constructor, which breaks encapsulation and makes the environment rigid. It also lets me override behavior — like changing max_outstanding from a test — without touching the agent or driver code.

The key pattern is set at a higher level and get at a lower level, matched by a string key like "vif" or "cfg".


Q3. ⭐ Why use the factory? What's the difference between new and type_id::create?

The factory lets me swap component or object types at runtime without editing the code that instantiates them.

new constructs a fixed, hard-coded type — whatever class name I wrote. type_id::create asks the factory which type to build, so a test can override it, for example replacing the base sequence item with an error-injecting subclass. For sequence items and components I always use create; new would defeat the purpose of the factory. In my environment, the derived tests rely on this — they override get_seq() to return different sequence types, all through the factory.


Q4. ⭐ How do the driver and sequencer communicate?

Through a TLM pull port — the driver pulls sequence items from the sequencer one at a time.

The driver calls get_next_item(), which blocks until a sequence produces an item; it drives that item onto the pins, then calls item_done(). On the sequence side, start_item and finish_item are the matching handshake. In my pipelined driver, the twist is that I call item_done() immediately after queuing the burst, not after it finishes on the bus — that's what lets the sequence run ahead and create multiple outstanding transactions.


Q5. Why is the monitor separate from the driver?

Because they answer different questions: the driver drives the protocol, the monitor passively observes what actually happened on the pins.

If the monitor reused the driver's internal knowledge, it would check the testbench's intent against itself, not against the DUT. Keeping it separate means the monitor reconstructs transactions purely from pin activity, so it can catch cases where the DUT did something different from what the driver sent. It also makes the monitor reusable in passive mode, where there's no driver at all.


Q6. Why is the analysis port 1:N?

So one observed transaction can fan out to multiple subscribers that each analyze it differently.

In my environment the monitor's analysis port connects to both the scoreboard and the coverage collector. The scoreboard asks "was it correct?" and coverage asks "did we exercise it?" — two independent questions on the same data. A 1:N port means I can add more subscribers, like a second coverage model or a logger, without changing the monitor at all.


Q7. ⭐ What happens if you don't raise an objection?

The run phase ends immediately, so the simulation finishes before the stimulus even runs.

Objections tell UVM "I still have work to do, don't end the phase." I raise one before starting the sequence and drop it after draining. Without it, run_phase would return right away. This is also why draining matters in my pipelined driver — I have to keep the objection raised until the driver goes idle, otherwise the last bursts get cut off mid-flight and the scoreboard never checks them.


Part 2: Verification Methodology

Q8. ⭐ How does the scoreboard know the correct answer?

It doesn't know in advance — it records observed writes into an independent reference model, then predicts read data from that model.

When the monitor reports a write burst, the scoreboard applies it to the reference model, storing the written bytes at spec-correct addresses. When a read comes back, the scoreboard asks the model what those addresses should contain and compares byte by byte. So the "answer" comes from the write values I actually observed, while the addressing rules come from the AXI spec — specifically the beat_addr() function that implements correct FIXED/INCR/WRAP addressing.


Q9. ⭐ Why build the reference model independently of the DUT?

If the model copies the DUT's implementation, it inherits the DUT's bugs, and both agree while both are wrong.

I wrote the model from the AXI spec, not from the RTL. That's exactly why it caught the WRAP bug: the model wraps addresses correctly per spec, while the DUT incremented them linearly. Because the two derive the address differently, they disagreed, and the scoreboard flagged it. A model built by reading the DUT code would have made the same mistake and passed silently.


Q10. ⭐ Does 100% functional coverage mean verification is done?

No — coverage only means I exercised those scenarios, not that they were correct.

Coverage answers "did I stimulate this?" while the scoreboard and assertions answer "was it right?" You can hit 100% coverage with a weak checker and still miss bugs. In fact I hit exactly this problem earlier in another project: my regression passed while an assertion was silently not firing — high coverage, blind checker. So coverage is necessary to show completeness, but it's meaningless without strong checking behind it.


Q11. ⭐ When do you use ignore_bins? What's the risk of overusing it?

I use it to exclude bins that are structurally unreachable, so they don't hold coverage below 100% forever.

For example, on a 32-bit bus a transfer size above 4 bytes can never happen, and a single-beat WRAP burst is illegal by spec, so those bins are ignored. The rule I follow is that every ignore must be justified — I can explain why it's physically impossible. The risk of overuse is hiding a real hole: if I ignore a bin just because it's hard to hit, I'm claiming coverage I don't actually have. That turns 100% into a lie.


Q12. ⭐ What's the difference between SVA and the scoreboard? Why didn't SVA catch the WRAP bug?

SVA checks protocol rules cycle by cycle; the scoreboard checks data correctness per transaction. They operate at different levels.

My assertions check things like VALID staying asserted until READY, payload stability, and WLAST landing on the right beat. The WRAP bug wasn't a protocol violation — every handshake was legal, WLAST was in the right place, the burst was well-formed. The bytes just landed at the wrong addresses. That's a data-correctness question, which only the scoreboard's byte-level comparison against the reference model could catch. So they're complementary: SVA guards the protocol, the scoreboard guards the data.


Q13. Can bugs remain even at 100% coverage?

Yes — coverage is only as complete as the coverage model I wrote.

If I never defined a coverpoint or cross for some condition, hitting 100% says nothing about it. Coverage measures what I chose to measure. A scenario I didn't think to model is invisible to the coverage number. That's why coverage, scoreboard, and assertions are three separate axes — no single one is sufficient.


Part 3: AXI Protocol

Q14. ⭐ What are the handshake rules? When can VALID be deasserted?

A transfer happens on any rising clock edge where both VALID and READY are high. VALID must stay asserted until that handshake completes.

The key rules: the source may assert VALID without waiting for READY — waiting on both sides would deadlock. Once VALID is high, it cannot drop until READY is seen. READY, on the other hand, can change freely. So VALID can only be deasserted after the handshake, never before. My assertions enforce exactly this — VALID held until READY, and payload stable while stalled.


Q15. Why does the 4KB boundary rule exist?

Because memory is managed in 4KB pages, and a single burst crossing a page could hit two regions with different properties or permissions.

A page boundary might separate cached from uncached memory, or valid from unmapped addresses. If one burst spanned that line, the slave couldn't handle it coherently. So AXI forbids a single burst from crossing a 4KB boundary. In my sequence item, a constraint enforces that the start offset plus the total transfer size stays within the page.


Q16. Why are WRAP lengths limited to 2, 4, 8, or 16 beats?

Because the wrap boundary is implemented by masking address bits, which only works cleanly when the window size is a power of two.

WRAP is used for cache-line fills, where the critical word is fetched first and the address wraps within the aligned line. To wrap, the hardware computes base = addr & ~(total-1), and that masking is only valid if total is a power of two — which forces the beat count to 2, 4, 8, or 16. In my item, a constraint restricts WRAP bursts to lengths 1, 3, 7, 15, which encode those beat counts.


Q17. In a narrow transfer, how is wstrb determined?

By the address and size — the active byte lanes are the ones the current beat's address maps to within the bus width.

If the transfer is narrower than the bus, only some lanes are valid each beat, and which lanes shift as the address increments. In my item, lane_mask() computes this: it takes the beat address modulo the bus width to find the starting lane, then sets size bytes from there. Randomized strobes are masked down to these legal lanes — deasserting some is still legal, but driving lanes outside the addressed range is not.


Q18. How does a slave support outstanding transactions?

By accepting a new address before the previous transaction's response is sent, usually with an internal queue that tracks in-flight requests by ID.

A single-outstanding slave holds AWREADY low until it finishes the current burst. A multi-outstanding slave buffers several accepted addresses and processes them in order or by ID. In my environment, the v2 DUT has an outstanding depth of 4, and my driver proves it reached the DUT using peak-concurrency counters — the single-outstanding v1 can never push those above 1.


Part 4: Project-Specific

Q19. ⭐ Why did you separate AW and W in the driver?

So the address and data channels run independently, which is what allows multiple bursts to overlap on the bus.

If I drove AW then W then waited for B in a single loop, a second address could never be issued while the first burst's data was still going — no outstanding transactions, so the slave's outstanding logic never gets exercised. Running AW, W, and B as separate threads lets the address channel keep issuing while data and responses proceed in parallel. That's the whole point of a pipelined driver.


Q20. ⭐ In a pipelined driver, how do you stop a read from overtaking a write?

I run writes and reads as separate passes with a drain in between, because the AXI read and write channels are independent.

If I mixed them freely, a read could reach the slave before an earlier write to the same address had completed, and I'd read stale data — a false failure. So in my outstanding test, phase one issues all the writes and drains until the driver is idle, then phase two issues the reads. That guarantees every write has landed before any read-back, preserving ordering across the independent channels.


Q21. ⭐ What symptom appears if the monitor can't keep up with outstanding transactions?

The monitor silently drops bursts, and the scoreboard reports false mismatches or missing transactions that look like DUT bugs.

This actually happened in my project — I saw 48 scoreboard failures and initially suspected the DUT. Root-causing it, the problem was the monitor: under overlapping traffic it lost track of a burst because it wasn't correlating address and data phases correctly. The fix was a channel-parallel monitor with FIFO-based correlation, one queue per phase, so it can reconstruct transactions no matter how many are in flight. The lesson was that a scoreboard failure isn't always a DUT bug — the environment can be wrong too.


Q22. ⭐ How did you confirm the assertions actually work?

With negative testing — I deliberately introduced a failing condition and confirmed the assertion fired.

This mattered especially because of a simulator quirk: Vivado XSim silently ignores assertions with untyped property arguments or dynamic arrays, which makes the whole checker vacuous with no error at all. So "no assertion failures" could mean the checks are passing, or it could mean they're not running. I proved they were live by writing a self-test assertion that must fail and confirming it did. That's the same principle as fixing a blind pass criterion — I verify that the checker fails when it should, not just that it passes.


Part 5: Deeper UVM (frequently asked follow-ups)

Q23. ⭐ What is a virtual interface and why is it needed?

It's a handle that lets dynamic UVM class objects reach the static hardware pins, which they otherwise can't touch.

UVM components are class objects created at runtime, but the DUT and interface are static hardware elaborated at time zero. A class can't directly connect to a physical signal. The virtual interface is a pointer to the actual interface instance, passed in through config_db, so the driver can drive vif.master_cb.awvalid and the monitor can sample it. Without it, there's no bridge between the object world and the pin world.


Q24. ⭐ What's the difference between a sequence and a sequence item?

A sequence item is one transaction — the data; a sequence is the program that generates a stream of them — the behavior.

The item, my axi4_seq_item, holds the address, length, size, burst type, and data for a single burst. The sequence's body() task decides how many items to create, with what constraints, and in what order — for example write-then-read-back, or a directed corner-case sweep. The same item is reused by every sequence; only the generation logic changes. That separation is why I can have one item class and a dozen sequences.


Q25. What is the difference between uvm_component and uvm_object?

Components are permanent and live in the hierarchy with phases; objects are transient data that flow through the components.

Driver, monitor, scoreboard, and agent are components — they're built once, have a parent, and run through build/connect/run phases. Sequence items and the reference model are objects — they're created and discarded freely and have no phases. A quick test: if it has a fixed place in the testbench tree and a lifecycle, it's a component; if it's data being passed around or randomized, it's an object.


Q26. ⭐ Walk me through what happens from run_test() to the first pin wiggle.

run_test builds the test, the test builds the environment top-down, ports connect bottom-up, then run_phase starts the sequence which feeds the driver.

run_test creates the test via the factory. The test's build phase creates the cfg, pulls the virtual interface from config_db, and builds the env. The env builds the agent, scoreboard, and coverage; the agent builds the sequencer, driver, and monitor. In connect phase, the driver's port binds to the sequencer's export and the monitor's analysis port binds to the scoreboard and coverage. Then run_phase raises an objection, starts the sequence, the sequence produces an item, the driver pulls it with get_next_item, and drives it onto vif — that's the first pin wiggle.


Q27. How does UVM know when to end the simulation?

When all raised objections have been dropped, the run phase ends and UVM moves to the cleanup phases.

Every component that has ongoing work raises an objection; the phase stays alive as long as any objection is outstanding. In my test, I raise one before the sequence and drop it after draining the pipeline. Once dropped and no one else is holding one, UVM proceeds to extract, check, and report phases, where the scoreboard and coverage print their final results.


Q28. Why randomize with constraints instead of just writing directed tests?

Constrained-random explores combinations I wouldn't think to write by hand, while constraints keep every generated case legal.

Directed tests only cover what I explicitly code, so I'll miss unexpected corners. Constrained-random generates legal-but-surprising combinations and, with enough seeds, fills coverage far faster. But randomness alone plateaus — in my project it stalled at 98.3%. So the real strategy is both: constrained-random for breadth, then directed sweeps to close the specific bins randomness kept missing. Constraints are what make the random stimulus legal in the first place.


Part 6: Debugging & Pressure Questions

Q29. ⭐ A test is failing. Walk me through how you debug it.

First I decide whether it's a DUT bug or an environment bug, because those lead to completely different fixes.

I start from the scoreboard error — it tells me the beat, lane, address, expected value, and actual value. I check whether the reference model's prediction is right by hand for that one transaction; if the model is wrong, it's an environment bug. If the model is right, I look at the waveform at that exact address and time to see what the DUT actually drove. I also check whether the monitor reconstructed the transaction correctly, because I've been burned before by the monitor dropping bursts and masquerading as a DUT bug. Only once I've localized it do I fix — and I re-run to confirm the fix and confirm I didn't mask something else.


Q30. ⭐ How do you know your testbench itself is correct?

I verify that it fails when it should — a checker that never fails is worthless, and a passing test proves nothing on its own.

Two concrete ways I did this. First, negative testing on the assertions: I injected a deliberately failing self-test and confirmed it fired, because XSim silently ignores certain assertion forms and would otherwise pass vacuously. Second, running the regression against a known-buggy DUT revision — it must fail on v1 and pass on v2. If a testbench passes on buggy hardware, the testbench is broken. That's the same lesson as fixing a blind pass criterion: I check that failure is detectable, not just that success is reported.


Q31. Your reference model and the DUT disagree. How do you know which one is wrong?

I work out the correct answer from the spec by hand for that single case, independent of both.

The reference model and the DUT are both just implementations that could be wrong. So I go back to the AXI spec and manually compute what should be at that address for that burst. Whichever matches the spec is right. In the WRAP case, I traced the wrap window by hand — base address, window size, wrap point — and the reference model matched the spec while the DUT didn't. That confirmed it was a real DUT bug, not a modeling error.


Q32. What would you add if you had more time on this project?

Error-response testing and out-of-order ID handling — two things the current DUT and environment don't exercise.

The DUT hardwires responses to OKAY, so I ignore the error-response bins today; with a DUT that can signal SLVERR or DECERR, I'd add stimulus and checks for those. I'd also test out-of-order completion by ID, which a more capable slave supports and my current single-ID-per-burst approach doesn't stress. On the environment side, I'd add random backpressure on the master's READY signals to exercise the slave's response-buffering under stall, which my always-ready driver doesn't cover today.


Q33. How would this environment scale to multiple masters or a full interconnect?

I'd instantiate multiple agents, make the scoreboard interconnect-aware, and add ID-based routing checks.

Each master becomes its own active agent with its own sequencer and driver, all reusing the same item and interface. The scoreboard has to become aware of which master owns which transaction — typically by ID or by a per-master address map — so it predicts and checks per source. I'd also add assertions for interconnect properties like ID uniqueness and response routing. The agent/env structure is built for this: adding an agent doesn't change the existing components.


Q34. What's the hardest part of verifying outstanding transactions specifically?

Correlation — matching each response back to the right outstanding request when several are in flight at once.

On the driving side it's easy to just keep issuing addresses. The hard part is the monitor and scoreboard: when responses can come back while multiple bursts are still open, the monitor has to correlate the address phase with the correct data and response phase. I solved it with one FIFO per phase, moving a transaction from queue to queue as each phase completes. If that correlation is even slightly wrong, transactions get mismatched and you get false failures that look exactly like DUT bugs.


Rapid-fire summary (one-liners to memorize)

build_phase        → top-down (parent builds child)
connect_phase      → bottom-up (wire TLM ports)
config_db          → set high, get low, by string key
factory/create     → runtime type override
virtual interface  → bridges class objects to static pins
get_next_item      → driver pulls from sequencer
analysis port 1:N  → one transaction, many subscribers
objection          → keeps run_phase alive; all dropped → sim ends
component vs object → hierarchy+phases vs transient data
sequence vs item   → behavior (program) vs data (transaction)
reference model    → independent, spec-based (not DUT-based)
coverage 100%      → "exercised", not "correct"
ignore_bins        → unreachable only, must justify
SVA                → protocol, cycle-level
scoreboard         → data, transaction-level
4KB boundary       → pages have different properties
WRAP 2/4/8/16      → power-of-two for bit masking
AW/W split         → enables outstanding
drain between W/R  → prevents read overtaking write
negative test      → proves assertions aren't vacuous
debug first step   → DUT bug or environment bug?
outstanding hard   → correlation, one FIFO per phase

The three stories that tie it together

If an interviewer asks an open-ended "tell me about a hard bug," these three carry the whole interview:

1. The WRAP bug (data correctness)
Independent reference model wrapped correctly, DUT incremented linearly. A symmetric WRAP-write / WRAP-read hid it because the DUT was consistently wrong on both sides. I designed an asymmetric WRAP-write / INCR-read cross-check that exposed 24 byte-level mismatches.

2. The 48 false failures (environment vs DUT)
Scoreboard reported failures I first blamed on the DUT. Root cause was the monitor silently dropping bursts under outstanding traffic. Taught me a scoreboard failure can be an environment bug.

3. The vacuous checker (verifying the verification)
XSim silently ignored assertions with untyped arguments — the checker looked like it passed but wasn't running. I proved liveness with a deliberately failing self-test. Same principle as fixing a blind regression pass criterion.

Common thread: I don't just find bugs — I understand why they stayed hidden, and I change the verification method to expose them.

profile
Design Verification engineer

0개의 댓글