UVM
Intermediate UVM Interview Questions
The intermediate-level UVM interview questions that probe whether you can actually build and connect a working, reusable environment — the sequencer-driver handshake and sequence layering, the config DB and factory overrides in practice, TLM connections and analysis, and phasing and objections in real testbenches — with model answers that show the mechanism, the practical detail, and the common pitfall, because the intermediate level is the practical exam that separates naming the parts from assembling them.
UVM Interview Mastery · Module 29 · Page 29.2
What Intermediate UVM Interviews Test
The beginner level established that you understand the parts — what a driver is, why the factory exists. The intermediate level is the practical exam: can you actually wire the parts together into a working, reusable environment, and fix it when it doesn't work? The questions shift from "what is X?" to "how do you do X?" and "what happens if Y?" — the sequencer-driver handshake (not "what is a sequencer" but how get_next_item/item_done actually work and what hangs if you forget item_done), sequence layering (how sequences call sequences, virtual sequences coordinate), the config DB and factory overrides in practice (not "what is the factory" but how you actually register an override in a test, how the virtual interface gets to the driver), TLM and analysis (how you connect a monitor to a scoreboard, what you do for two analysis inputs), and phasing and objections in real testbenches (how a test actually raises and drops objections, uses a default sequence). The interviewer is probing for hands-on experience — the practical details and pitfalls that only show up when you've actually built environments — because the intermediate level separates someone who knows what the components are from someone who can assemble them. The skill this chapter builds is answering with the mechanism, the practical detail, and the common pitfall — because the intermediate interviewer wants to see you've turned the wrenches, not just read the manual. This chapter is the intermediate question sets — the handshake and sequences, config and factory in practice, TLM and analysis, phasing and objections in working testbenches, and building a real environment — with model answers that show the hands-on grasp.
Intermediate UVM interview mastery is demonstrating you can build and connect a working, reusable environment — the practical mechanics of the sequencer-driver handshake, sequence layering, the config DB and factory in practice, TLM connections and analysis, and phasing and objections in real testbenches — with answers that show the mechanism, the practical detail, and the common pitfall. The topics tested: the sequencer-driver handshake (get_next_item/item_done, the blocking handshake, the body task, start, sequence layering and virtual sequences/sequencers); the config DB and factory overrides in practice (config objects, the virtual interface flow, registering a type or instance override in a test, set/get scoping); TLM connections and analysis (connecting a monitor's analysis port to a scoreboard, two analysis imps with the decl macro, building a scoreboard's connections); phasing and objections in working testbenches (a test raising and dropping objections, default sequences, the scheduled sub-phases); and building and connecting a real environment (the agent's build and connect, env composition, configuration, field automation). The meta-skill: answer like a mechanic, not a manual — give the mechanism, then the practical detail (the actual call, the gotcha), then the pitfall (what hangs, what silently fails) — because the intermediate interviewer is running a practical exam, and the pitfall awareness is the tell of hands-on experience. This chapter is the intermediate question sets and how to answer them with practical depth.
What do intermediate UVM interviews test — the sequencer-driver handshake and sequence layering, the config DB and factory overrides in practice, TLM and analysis connections, and phasing and objections in working testbenches — and how do you answer with the mechanism, the practical detail, and the pitfall that show you can actually build a real environment, not just name its parts?
Motivation — why the practical mechanics are the real screen
The intermediate questions screen for hands-on experience — they separate candidates who've built environments from those who've only read about them — and the practical details and pitfalls are what reveal it. The reasons they matter:
- Knowing the parts isn't building the environment. Anyone can name the components. The intermediate question — "how do you connect a monitor to a scoreboard?" — requires you to know the actual mechanism (analysis port to analysis export, in connect_phase), which you only know if you've built it.
- The pitfalls are the experience tell. "What happens if you forget
item_done?" — the sequence hangs infinish_item— is something you learn by debugging it, not by reading the definition. Pitfall awareness is the clearest signal of hands-on experience. - "What happens if Y?" probes depth. The interviewer follows the mechanism with a failure scenario — "what if the config-DB paths don't match?" — because the failure mode is where understanding runs deeper than the happy path.
- Intermediate is the real working level. Most verification work is intermediate: building agents, wiring environments, writing sequences, connecting scoreboards, debugging the wiring. The intermediate questions are the actual job, so they're the questions that matter most for a working role.
- Practical depth sets up the advanced questions. Advanced questions build on the practical mechanics — you can't discuss reusable sequence libraries if you can't write a basic sequence and start it. Solid intermediate mechanics are the platform for senior topics.
The motivation, in one line: the intermediate questions screen for hands-on experience by requiring the actual mechanism and probing the pitfalls — which you only know if you've built and debugged real environments — and intermediate is the real working level (building, wiring, debugging), so mastering the practical mechanics with their pitfalls is what proves you can do the job and sets up the advanced level.
Mental Model
Hold answering intermediate UVM questions as being a mechanic who can assemble and tune the engine, not just name its parts:
Two people can both correctly name every part of an engine — carburetor, distributor, timing belt, fuel injector. On a written quiz, they look identical. But hand them a box of those parts and an engine block, and the difference is total. One has only read the manual: they know what each part is for, but they've never installed one, so they fumble the connections, don't know the order things go together, can't tell a properly seated part from a loose one, and when the assembled engine won't start, they have no idea where to begin — they can recite what a fuel injector does but can't diagnose why this one isn't delivering fuel. The other is a mechanic who has turned the wrenches: they install each part in the right order, make the connections correctly and know how to check they're tight, tune the adjustments by feel, and when the engine won't start, they run a practiced diagnostic — is it getting fuel, is it getting spark, is the timing right — and find the problem fast, because they've fixed this before. The diagnostic ability is the giveaway. Anyone who has actually assembled and run engines has also had them not work, and has learned the failure modes and how to chase them; someone who has only studied has the vocabulary but not the debugging instinct. So if you want to know whether someone can actually work on engines, you don't ask them to name the parts — you hand them a non-running engine and watch how they diagnose it. The fluency under a real fault is what no amount of manual-reading produces. Two people can both correctly name every part of an engine. On a written quiz, they look identical. But hand them a box of parts and an engine block, and the difference is total. One has only read the manual: they know what each part is for but never installed one, so they fumble the connections, don't know the order, can't tell a seated part from a loose one, and when the engine won't start, they have no idea where to begin — they can recite what a fuel injector does but can't diagnose why this one isn't delivering fuel. The other is a mechanic who has turned the wrenches: they install in the right order, make connections correctly and check they're tight, tune by feel, and when the engine won't start, they run a practiced diagnostic — fuel, spark, timing — and find the problem fast, because they've fixed this before. The diagnostic ability is the giveaway: anyone who has actually assembled and run engines has had them not work, and learned the failure modes; someone who only studied has the vocabulary but not the debugging instinct. So to know whether someone can actually work on engines, you don't ask them to name the parts — you hand them a non-running engine and watch how they diagnose it. The fluency under a real fault is what no amount of manual-reading produces.
So answering intermediate UVM questions is being the mechanic, not the manual-reader: the question ("how do you connect a monitor to a scoreboard?", "what happens if you forget item_done?") is the box of parts and the non-running engine — and you answer with the mechanic's fluency: the mechanism (how the parts go together — analysis port to export in connect_phase; the driver loops get_next_item/item_done), the practical detail (the actual call, the order, how you check it's right), and — crucially — the pitfall and the diagnostic (forgetting item_done hangs the sequence in finish_item; an unconnected analysis port lets the scoreboard silently check nothing; here's how you'd catch it). The pitfall-and-diagnostic is the fluency under a real fault that proves you've turned the wrenches — it's what the manual-reader can't fake. The practical form: for every intermediate question, answer in three beats — the mechanism (how it works), the practical detail (the actual call and gotcha), and the pitfall (what fails and how you'd diagnose it) — which is exactly the mechanic's fluency the interviewer is probing for. Answer intermediate questions like a mechanic handed a non-running engine — give the mechanism, the practical detail, and the pitfall-with-diagnostic — because the intermediate interview is a practical exam, and fluency under a real fault is what proves you've built and debugged environments, not just read about them. Show you can assemble it and diagnose it, not just name it.
The Intermediate Topic Map
The defining picture is the map of what the intermediate level covers — the five practical areas you must be able to build and debug.
The figure shows the intermediate UVM topic map. The sequencer-driver handshake + sequence layering (the brand-colored — the core mechanic): get_next_item/item_done, the body task, start, and sequences/virtual sequences coordinating stimulus. Config DB + factory overrides in practice (success-colored): the virtual-interface flow, config objects, and registering type/instance overrides in a test. TLM connections and analysis (the warning-colored — the silent-failure area): connecting a monitor's analysis port to a scoreboard, and handling multiple analysis inputs. Phasing and objections in working testbenches (success-colored): a test raising/dropping objections, default sequences, and the scheduled sub-phases. Building and connecting a real environment (default-colored): the agent's build/connect, env composition, configuration, and field automation. The crucial reading is that every area is probed with "how do you?" and "what happens if?" — so you command each as buildable mechanics with their pitfalls, not as definitions. The brand-colored handshake is highlighted as the core mechanic because it's the most commonly asked and the richest in pitfalls (the blocking handshake hangs in characteristic ways). The warning-colored TLM/analysis is flagged because its failures are silent — an unconnected analysis port doesn't error, so the pitfall awareness (a scoreboard receiving nothing passes green) is especially distinguishing. The map is roughly the build order of an environment: write sequences and the handshake, configure via the config DB and factory, connect via TLM, control via phasing/objections, all composed into the env — so commanding the map is commanding how you'd actually build a testbench. The diagram is the intermediate syllabus: handshake/sequences → config/factory → TLM/analysis → phasing/objections → building the env, each as buildable, debuggable mechanics. Master the five intermediate areas as buildable mechanics with their pitfalls — the handshake, config and factory in practice, TLM and analysis, phasing and objections, and composing the env — because each is probed with how-do-you and what-happens-if questions.
Question Set — The Sequencer-Driver Handshake and Sequences
The sequencer-driver handshake is a blocking, two-sided protocol that paces transactions from sequences to the driver. On the driver side, the run phase is a forever loop: it calls seq_item_port.get_next_item, which blocks until a transaction is available; when one arrives, it drives the transaction onto the interface as signal-level activity; then it calls seq_item_port.item_done to signal completion. On the sequence side, the body task creates a transaction, calls start_item to request access to the sequencer, randomizes the item, and calls finish_item, which blocks until the driver has signaled item_done. So the flow is: the sequence does start_item, randomize, finish_item; finish_item sends the item to the sequencer, which arbitrates and hands it to the driver's waiting get_next_item; the driver drives it and calls item_done; that unblocks the sequence's finish_item, which returns, letting the sequence produce its next item. The two blocking points are the key: the driver blocks in get_next_item waiting for an item, and the sequence blocks in finish_item waiting for item_done. This blocking is what paces the stimulus to the driver's readiness — the sequence can't run ahead of what the driver has consumed. There's also a get_next_item/item_done variant versus a get/put style, and a peek variant, but the get_next_item/item_done pair is the standard. The practical detail to mention is that item_done is mandatory after each get_next_item, and the pitfall, which proves you've debugged it, is that if the driver forgets to call item_done, the sequence's finish_item never returns, so the sequence hangs, having driven exactly one transaction and then stalling forever. The understanding to convey is the two-sided blocking handshake — both sides block on the other — and the characteristic failure of forgetting item_done, which is exactly the what-happens-if the interviewer will ask.
Nothing driving means the driver is blocked in get_next_item with no item arriving, so it's a producer-side problem, and I'd check, in order, whether a sequence was started, on which sequencer, and whether the driver even reaches get_next_item. First, the most common cause: was a sequence actually started? A sequence has to be started on a sequencer, either explicitly with seq.start(sequencer) or by being set as the phase's default sequence, and if nothing started one, the driver waits forever with nothing to drive. I'd confirm a sequence is started. Second, if a sequence is started, is it on the right sequencer? If it's started on a different agent's sequencer, its items go to that agent's driver, not the one feeding the interface I care about, so my interface stays idle while another might be active. I'd check the sequencer the sequence targets matches the driver of the interface. Third, related to phasing: did the run phase even stay alive? If no objection was raised, the run phase ends in zero time before the sequence can drive anything — so I'd check that an objection is raised, or that the sequence is a default sequence which manages the objection. Fourth, is the driver itself reaching get_next_item? If the driver's run phase has a bug before the loop, or the driver wasn't built, or its virtual interface is null, it might not get to driving. I'd raise verbosity on the driver and sequencer to see the handshake events — whether get_next_item is reached, whether a sequence started. The way I'd diagnose this is to recognize the symptom: an idle interface while the clock runs and the test doesn't end points to the sequencer or sequence, the producer side, not the DUT, because the DUT can't respond to stimulus that never arrives. The understanding to convey is the systematic check — sequence started, correct sequencer, objection raised, driver reaching the loop — and that you localize to the producer side because that's where an idle interface comes from, which shows debugging experience rather than guessing.
Sequences layer by calling other sequences from within a sequence's body, building complex stimulus from simpler pieces, and a virtual sequence is a top-level sequence that coordinates multiple sequencers — multiple agents — rather than driving a single one. Layering within one agent: a sequence's body can start other sequences on the same sequencer, so you build a library of small, focused sequences — a single write, a burst, a specific corner case — and compose them into larger scenarios by having a higher-level sequence call them. This is reuse: the small sequences are written once and combined many ways. You start a sub-sequence from a parent sequence using its start method with the sequencer, or the do macros, running it on the same sequencer the parent runs on. A virtual sequence solves a different problem: coordinating stimulus across multiple interfaces. In a real environment with several agents — say a bus agent, an interrupt agent, a config agent — you often need to coordinate them: configure via one, then drive traffic on another while handling interrupts on a third. A virtual sequence runs on a virtual sequencer, which doesn't drive an interface itself but holds handles to the real sequencers of the agents. The virtual sequence's body starts sub-sequences on those real sequencers — through the handles, often accessed via a p_sequencer reference — orchestrating the multi-agent scenario from one place. So it's virtual because it doesn't drive pins; it conducts the real sequencers. The practical detail is the p_sequencer mechanism — declaring the sequence to run on the virtual sequencer type so it can reach the sequencer handles — and the setup of the virtual sequencer holding references to the agents' sequencers, wired in connect phase. The understanding to convey is the two kinds of layering — sequences composing sequences on one sequencer for reuse, and virtual sequences coordinating multiple sequencers for multi-agent scenarios — and the role of the virtual sequencer as the conductor that holds the real sequencer handles, which is a clear intermediate-level practical topic.
The body task is where a sequence defines what it does — it's the method that generates and sends the sequence's transactions — and you start a sequence by calling its start method with the target sequencer, or by setting it as a phase's default sequence. The body task is overridden in your sequence class and contains the logic: it creates sequence items, randomizes them, and sends them to the driver through the sequencer using the start_item and finish_item handshake, or the convenience do macros that wrap that handshake. For example, a body might loop a number of times, each iteration creating an item, randomizing it with any in-line constraints, and finishing it; or it might start sub-sequences. The body is a task because sending items through the blocking handshake consumes time. To run a sequence, the most direct way is seq.start(sequencer), where you construct the sequence, optionally configure it, and call start passing the sequencer it should run on; start invokes the body and blocks until the sequence completes. You call this typically from a test's run phase, often bracketed by raising and dropping an objection so the phase stays alive while the sequence runs. The alternative is to set the sequence as the default sequence for a phase on a sequencer, using the config DB to set the default_sequence; then UVM automatically starts it when that phase runs on that sequencer, and manages the objection through the sequence's starting_phase. The practical details worth mentioning are the start_item/finish_item versus the do macros for sending items, that body is a task because it consumes time, and the two ways to run a sequence — explicit start versus default sequence. A pitfall to note is that if you start a sequence in run phase without raising an objection, and it's not a default sequence managing the objection itself, the phase can end before or during the sequence. The understanding to convey is what body contains — item generation via the handshake — and the mechanics of starting, both explicit and via default sequence, with the objection consideration, showing you've actually written and run sequences.
Question Set — The Config DB and Factory Overrides in Practice
The virtual interface travels through the config DB: the top-level testbench sets it into the database, and the driver and monitor get it out in their build phase. The flow starts in the static testbench top — the module that instantiates the DUT and the interface and starts the UVM test. There, the actual interface instance exists, and the top does a uvm_config_db set, parameterized by the virtual interface type, specifying a path that targets the components that need it, a field name like vif, and the interface handle. Then, deep in the testbench, the driver and monitor, in their build phase, do a uvm_config_db get with the matching field name to retrieve the virtual interface handle into their local vif variable, which they then use to drive or sample the signals. So the config DB decouples the top, which has the interface, from the driver, which needs it, without threading the handle manually through every level of the hierarchy. The matching is on the hierarchical path, the field name, and the type, and the set must happen before the get — which it does, because the top sets it before the test's components build. The practical details: the set is often done with a wildcard or broad path so it reaches the consumers regardless of exact naming, the field name string must match exactly between set and get, and the parameterized type must match. The pitfall, which is one of the most common bring-up bugs, is that if the set and get don't match — a wrong path, a field-name typo, a type mismatch — the get fails silently, the vif stays null, and the testbench crashes not at the get but later, at the first use of the interface, with a null-object access. So when a virtual interface is null and crashes in the run phase, the cause is usually a config-DB set/get mismatch, found by enabling the config-DB trace and comparing the set and get. The understanding to convey is the set-in-top, get-in-driver flow through the config DB, why it decouples top from driver, and the null-vif pitfall from a mismatch — which shows you've wired up and debugged a real interface connection.
You register a factory override in the test's build phase, before the environment is built, using set_type_override or set_inst_override on the type you want to replace — and it takes effect for every construction through the factory after that point, which is why it must be set before the target is created. In practice, in a test that wants to substitute a behavior, you override in build_phase, typically before calling super.build_phase, which is what builds the environment. For a type override, you call the base type's type_id::set_type_override passing the override type's get_type, which says: anywhere the factory constructs this base type, construct the override instead, globally. For an instance override, you use set_inst_override with a hierarchical path, targeting just one instance. Common uses: a fault-injection test overrides the normal driver with an error-injecting driver; an extended monitor with extra checking replaces the base monitor; a specialized sequence item replaces the base item. The timing is critical: the override must be registered before the factory creates the target. Since the environment's components are created in the build phase, and build runs top-down with the test at the top, setting the override in the test's build phase before super.build_phase ensures it's in place before the environment builds the components it affects. If you set the override after super.build_phase, the components are already constructed as the base type, and the override is too late. The pitfall, beyond timing, is that the override only applies to components constructed through the factory with create — if a component is built with a direct new, the override silently doesn't take, and the base type runs. So you confirm an override worked by printing the topology to see the override type was actually built, not just by printing the factory to see it's registered. The understanding to convey is where and when you register the override — test build phase, before super.build_phase — why the timing matters, and the create-not-new and confirm-with-topology practical points, which show you've actually used overrides to customize tests.
A config object is a single uvm_object that bundles together the configuration for a component or subsystem — its fields hold the various settings — and you pass it through the config DB as one unit, instead of setting many individual values separately. Rather than doing a separate config-DB set for each setting — is_active here, data_width there, an address map somewhere else — you define a config class extending uvm_object with fields for all of them, construct and populate one instance, and set that single object into the config DB. The component does one get to retrieve the whole config object, then reads its fields. For an agent, you might have an agent_config with is_active, the virtual interface, and protocol options; for an environment, an env_config that contains the sub-configs for each agent. Why this is better than many individual sets: it's cohesive — all of a component's configuration is in one place, one object, so you can see and manage it together; it's structured — an environment config can contain agent configs, mirroring the hierarchy, so configuration composes the way the components do; it's fewer config-DB transactions and fewer chances for a path or field-name mismatch, since it's one set and get instead of many; and it's reusable and overridable — the config object is itself a uvm_object you can extend or randomize. The typical pattern is that the test creates and configures the env config object, sets it in the config DB, the env gets it and distributes the sub-configs to its agents, and each agent gets its config and configures itself. The practical detail is that the config object is constructed and set high up, usually in the test, and consumed down the hierarchy, with the structure of nested configs matching the component hierarchy. The understanding to convey is that a config object bundles related settings into one structured, reusable unit passed as a whole, which is cleaner, more cohesive, less error-prone, and composes hierarchically — the practical way real environments are configured rather than scattering individual sets.
You make an agent active or passive by reading an is_active configuration field in the agent's build phase and conditionally constructing the driving components — an active agent builds the sequencer and driver as well as the monitor, while a passive agent builds only the monitor. The mechanism: the agent has a configuration field, conventionally is_active of type uvm_active_passive_enum, set to UVM_ACTIVE or UVM_PASSIVE, typically delivered via the agent's config object or a direct config-DB set. In the agent's build_phase, after getting its configuration, it checks: if is_active is UVM_ACTIVE, it constructs the sequencer and the driver, in addition to the monitor; if UVM_PASSIVE, it constructs only the monitor and skips the sequencer and driver. Then in connect_phase, the connections that involve the driver and sequencer — connecting the driver's seq_item_port to the sequencer's seq_item_export — are also guarded by the is_active check, since those components only exist in the active case; the monitor's analysis port connection happens in both. So the same agent class produces a full driving agent or a monitor-only agent depending on one config field. Why: this is reuse by configuration across integration levels, as discussed — active at block level where the agent drives, passive at SoC level where the real neighboring block drives and the agent only observes and collects coverage. Rather than two separate agent classes, you have one configurable agent. The practical details are guarding both the build of the driver and sequencer and their connection in connect phase on the is_active check, and getting is_active from the config before deciding. A pitfall is forgetting to guard the connect-phase connections, which would try to connect a driver that wasn't built in the passive case, causing a null-handle error. The understanding to convey is the conditional construction in build phase plus guarded connection in connect phase based on is_active, and that it serves reuse across integration levels — showing you can actually build a configurable agent, not just describe active versus passive.
Question Set — TLM Connections and Analysis
You connect a monitor to a scoreboard through TLM analysis: the monitor has an analysis port, the scoreboard has an analysis export or implementation, and you connect them in the connect phase by calling connect on the port, passing the export. Concretely, the monitor declares a uvm_analysis_port parameterized by the transaction type, and in its run phase, every time it reconstructs a transaction from the interface, it calls write on that port, broadcasting the transaction. The scoreboard declares a uvm_analysis_imp — an analysis implementation — parameterized by the transaction type and the scoreboard type, and provides a write method that receives each transaction and does the checking. The connection is made in the connect phase of whatever component contains both — usually the environment: env.connect_phase calls agent.monitor.analysis_port.connect(scoreboard.analysis_export). Once connected, every monitor write reaches the scoreboard's write method. The analysis port is one-to-many and non-blocking — the monitor writes and moves on, and any number of subscribers can be connected, so the same monitor output can also feed a coverage collector by connecting its analysis port to the coverage subscriber too. The practical detail is that this connection lives in connect phase, after build has created both the monitor and scoreboard, and that the monitor's write is what drives the flow. The pitfall, which is important and silent, is that if you forget to make the connection, nothing errors — an unconnected analysis port is legal because analysis ports permit zero subscribers — so the monitor's write goes into the void, the scoreboard's write is never called, the scoreboard compares nothing, and the test passes green having checked nothing. So you confirm the connection with a topology print and by checking the scoreboard actually received transactions, never trusting a green result from a scoreboard you haven't confirmed is connected. The understanding to convey is the analysis-port-to-export connection in connect phase, the one-to-many broadcast nature, and the silent-failure pitfall of an unconnected port — which shows you've wired and debugged real scoreboard connections.
When a scoreboard needs more than one analysis input, you use the uvm_analysis_imp_decl macro to create distinct, suffixed analysis implementations, because two plain analysis imps would both require a write method and collide. The problem: a uvm_analysis_imp requires the component to implement a method named write. If a scoreboard has two analysis inputs — say one from the input-side monitor and one from the output-side monitor, to compare them — and you declare two plain analysis imps, both require a write method on the scoreboard, and you can't have two methods with the same name and signature; they collide. The solution is the decl macro. You invoke uvm_analysis_imp_decl with a suffix for each input — for example, _before and _after, or _in and _out. Each invocation generates a specialized imp type, like uvm_analysis_imp_in and uvm_analysis_imp_out, that requires a correspondingly suffixed write method — write_in and write_out. So in the scoreboard you declare the two ports as those suffixed imp types, and implement write_in and write_out as separate methods, each handling its stream. They no longer collide because each routes to its own distinctly-named method. Then in connect phase, you connect the input monitor's analysis port to the scoreboard's input imp and the output monitor's to the output imp. The typical scoreboard pattern is then to queue or buffer transactions from one stream and match against the other — write_in might enqueue an expected transaction predicted from the input, and write_out might compare an observed output against the queue. The practical detail is exactly this — the decl macro per input with a distinct suffix, and the matching suffixed write methods — and the pitfall is forgetting the decl macro and getting a compile error about write, or mis-wiring which monitor connects to which imp. The understanding to convey is that multiple analysis inputs need uvm_analysis_imp_decl to create distinct suffixed write methods, why — to avoid the write-method collision — and how you'd then connect and use them, which is a clear sign you've built a multi-input scoreboard.
In practice, an analysis port is a non-blocking, one-to-many broadcast used for distributing observed transactions to checkers and coverage, while put and get ports are blocking, point-to-point connections used for handing transactions between two specific components, like a sequencer and driver. The analysis port, uvm_analysis_port, is what monitors use to broadcast. Its write is non-blocking — the monitor calls write and immediately continues, it doesn't wait for anyone — and it's one-to-many, so zero or more subscribers can connect, each receiving every transaction. It's for the analysis path: distributing what the monitor observed to scoreboards and coverage, which passively consume. Crucially, it tolerates zero subscribers without error, which is why an unconnected one fails silently. Put and get ports are for point-to-point transfer where the producer and consumer are paired. A blocking put port blocks until the peer takes the item; a get port blocks until an item is available. These are used where one component hands work to exactly one other and needs the transfer to complete — the canonical example being internal to the sequencer-driver mechanism. Because they're point-to-point and required, UVM checks at end of elaboration that a required port like a blocking put is connected, and fatals if not — the opposite of the analysis port's silent tolerance of being unconnected. So the practical differences that matter: analysis is broadcast, non-blocking, many subscribers, silently tolerant of zero; put/get is point-to-point, blocking, one peer, loudly required. You use analysis for the monitor-to-scoreboard-and-coverage distribution, and put/get style for paired handoffs. The pitfall difference is the key practical point — an unconnected analysis port is silent, an unconnected required port is a loud connect-phase fatal — so the failure modes are opposite, and you debug them differently. The understanding to convey is the broadcast-versus-point-to-point and non-blocking-versus-blocking distinction in their actual uses, and especially the opposite failure modes, which shows practical TLM experience.
Question Set — Phasing and Objections in Working Testbenches
In a working test, the run phase raises an objection at the start, runs its stimulus, and drops the objection when the stimulus is done, which keeps the phase alive exactly as long as needed. The concrete pattern in a test's run_phase task is: call phase.raise_objection(this) to keep the phase alive; create the sequence with the factory; start it on the environment's sequencer with seq.start, which blocks until the sequence completes; then call phase.drop_objection(this). When the objection is dropped and the count returns to zero, the run phase ends. So the test brackets its stimulus between raising and dropping the objection. The reason this is necessary is that the run phase ends when its objection count is zero, and the count starts at zero — so without raising an objection, the phase would end immediately, in zero time, before the sequence drives anything, and the test would pass having done nothing. Raising the objection holds the phase open while the sequence runs; dropping it when the sequence is done lets the phase end at the right time. In more complex tests, multiple objections may be in play — several agents or sequences each holding one — and the phase ends when all are dropped. An alternative that avoids manual objection management is to set the sequence as the phase's default sequence via the config DB; then UVM starts it automatically and manages the objection through the sequence's starting_phase, raising when it starts and dropping when it ends. The practical details are the raise-start-drop structure, that start blocks until the sequence finishes so the drop happens after, and the default-sequence alternative. The two pitfalls, which show real experience, are forgetting to raise — the test finishes in zero time having driven nothing, often passing green and hiding the problem — and forgetting to drop — the phase never ends and the test hangs. The understanding to convey is the concrete raise-stimulus-drop pattern, why it's needed given the count starts at zero, and the two failure directions, demonstrating you've actually written tests that control the run phase.
The scheduled run-time sub-phases are a set of finer-grained phases that run during the run-time, in order — pre_reset, reset, post_reset, pre_configure, configure, post_configure, pre_main, main, post_main, pre_shutdown, shutdown, post_shutdown — and you use them when you need to coordinate stimulus into distinct stages, like reset, then configuration, then main traffic, then shutdown, across the testbench. They run in parallel with the run_phase but provide structure the single run_phase doesn't. The grouping is meaningful: the reset phases are for applying and releasing reset; the configure phases for programming the DUT, like writing configuration registers; the main phases for the primary stimulus and traffic; the shutdown phases for draining and finishing cleanly. Each has pre and post variants for finer ordering. You'd use them when a test naturally has stages that must happen in order across multiple components — for instance, every agent should hold reset during the reset phase, then the config agent programs registers during the configure phase, then traffic agents drive during the main phase — and the scheduled phases give a common, ordered framework so all components synchronize their stage transitions. Sequences can be assigned as default sequences to specific sub-phases, so a reset sequence runs in the reset phase, a config sequence in configure, a traffic sequence in main. Objections work per sub-phase, so each phase stays alive while its stimulus runs. In practice, many testbenches use just the run_phase for simpler cases and reach for the scheduled sub-phases when the staging is complex enough to benefit from the structure. The practical detail is that they run concurrently with run_phase and that you assign sequences to them and manage objections per phase; a consideration is not mixing run_phase and the sub-phases carelessly since they run in parallel. The understanding to convey is the reset-configure-main-shutdown staging, when the structure helps — coordinated multi-stage scenarios across components — and how sequences and objections map to them, showing awareness beyond just the basic run_phase.
A virtual sequence coordinates multiple agents by running on a virtual sequencer that holds handles to the real sequencers, starting sub-sequences on each, and the objection is typically managed once at the virtual-sequence or test level, around the whole coordinated scenario. The setup: a virtual sequencer is a sequencer that doesn't connect to a driver but holds references to the actual sequencers of the agents in the environment, wired up in the environment's connect phase. The virtual sequence runs on this virtual sequencer, and through a p_sequencer reference — which gives the sequence typed access to the virtual sequencer and thus to the agent sequencer handles — its body starts sub-sequences on the specific agents' sequencers. So the virtual sequence's body might start a config sequence on the config agent's sequencer, wait for it, then start a traffic sequence on the bus agent's sequencer while concurrently starting an interrupt-handler sequence on the interrupt agent's sequencer, orchestrating the whole multi-agent scenario from one place. For objections, the clean approach is to raise one objection around the entire virtual sequence — either the test raises it before starting the virtual sequence and drops it after, or the virtual sequence itself raises and drops through its starting_phase if it's a default sequence — so the run phase stays alive for the whole coordinated scenario and ends when it completes. You generally don't want every sub-sequence managing its own objection independently, because the coordination is at the virtual-sequence level; the objection should bracket the coordinated whole. The practical details are the virtual sequencer holding the real sequencer handles wired in connect phase, the p_sequencer access, starting sub-sequences on the specific sequencers, and bracketing the whole with one objection. The understanding to convey is that the virtual sequence is the conductor — using the virtual sequencer's handles to start sub-sequences on multiple agents — and that the objection wraps the coordinated scenario as a whole, which shows you can build the multi-agent coordination that real SoC-level testbenches need.
Question Set — Building and Connecting a Real Environment
A simple agent is built by constructing its sub-components in build_phase and wiring them in connect_phase, with the active/passive distinction guarding the driving components. In build_phase, the agent first gets its configuration to know whether it's active or passive. It always constructs the monitor, since both active and passive agents monitor. If active, it also constructs the sequencer and the driver, using the factory's create so they're overridable. So build_phase ends with either a monitor alone, passive, or a monitor, sequencer, and driver, active. In connect_phase, the agent makes the connections. The always-present connection is exposing the monitor's analysis port — the agent typically has its own analysis port that it connects to the monitor's, so the environment can connect to the agent's port without reaching inside. The active-only connection is the sequencer-driver handshake: the driver's seq_item_port connects to the sequencer's seq_item_export, which is what lets the driver pull items from the sequencer. This connection is guarded by the active check, since the driver and sequencer only exist when active. The driver and monitor also need the virtual interface, which they get from the config DB in their own build phases. So the agent's structure is: build the components conditionally on active/passive, connect the monitor's port outward always, connect the driver to the sequencer only when active. The practical details are using create for the components, guarding both the construction and the connection of the driver and sequencer on is_active, and exposing the monitor's analysis port at the agent level for clean encapsulation. A pitfall is connecting the driver to the sequencer without guarding on active, which crashes in the passive case because they weren't built. The understanding to convey is the conditional build and guarded connect, the always-on monitor port exposure, and the sequencer-driver connection that enables the handshake — demonstrating you can actually assemble an agent, which is the fundamental reusable unit.
The environment composes them by constructing the agents and scoreboard in its build phase and connecting each agent's monitor output to the scoreboard in its connect phase, distributing configuration to the agents along the way. In build_phase, the env constructs its agents — through the factory — and its scoreboard, and any coverage collectors. It also handles configuration: if using config objects, the env gets its env config, which contains the per-agent configs, and sets each agent's config into the config DB before or as the agents build, so each agent comes up active or passive and configured correctly. In connect_phase, the env wires the transaction flow: it connects each agent's analysis port — the one exposing the monitor's output — to the scoreboard's corresponding analysis import. If the scoreboard has multiple inputs, the env connects the input-side agent to the input imp and the output-side agent to the output imp, using the suffixed imps from the decl macro. It similarly connects agent outputs to coverage collectors. If there's a virtual sequencer, the env constructs it and, in connect_phase, assigns the agents' real sequencers into the virtual sequencer's handles, so virtual sequences can reach them. So the env is the integration point: it instantiates the pieces, configures them, and connects the analysis flow from monitors to scoreboard and coverage, plus the virtual sequencer wiring. The practical details are constructing through the factory, distributing config objects down to the agents, connecting analysis ports to the scoreboard's imps in connect phase, and wiring the virtual sequencer's handles. A pitfall is forgetting one of the analysis connections, which silently leaves that scoreboard input or coverage collector receiving nothing. The understanding to convey is the env as composer and integrator — build the agents and scoreboard, configure the agents, connect the analysis flow and the virtual sequencer — which shows you can assemble a complete environment from the reusable pieces, the core intermediate skill.
Field automation is UVM's mechanism for automatically generating common object methods — like copy, compare, print, pack, and record — from a declaration of an object's fields, using the field macros, so you don't hand-write them; and convert2string, copy, and compare are among the methods it provides or that you commonly implement for transactions. When you define a transaction or sequence item, you can register its fields using the uvm_field macros inside the utils block — for each field, a macro like uvm_field_int names it and sets flags for which automated behaviors apply. From these declarations, UVM automatically provides implementations of methods like copy, which deep-copies one object into another; compare, which checks two objects for equality field by field; print, which formats the object's fields; pack and unpack, which serialize and deserialize; and record, for transaction recording. So with the field macros, you get all these methods for free, consistently, rather than writing each by hand for every transaction type. convert2string specifically returns a human-readable string representation of the transaction, used in logging and debug — for instance a scoreboard or driver logs a transaction with convert2string to show its fields; it's often hand-written for control over formatting, or you rely on the print infrastructure. copy is used when you need an independent duplicate of a transaction — for example, a monitor might copy an observed transaction before broadcasting it, so downstream consumers each have their own, or a scoreboard copies an expected transaction into its queue. compare is used in checking — a scoreboard compares an observed transaction against the expected, and the automated compare does the field-by-field equality, returning whether they match and logging mismatches. The practical trade-off worth mentioning is that field automation is convenient but has some runtime cost, and for performance-sensitive or large transactions some teams hand-write copy, compare, and convert2string instead of using the macros, via the do_copy, do_compare, do_print callbacks. The understanding to convey is that field automation generates the standard object methods from field declarations, what copy, compare, and convert2string each do and where they're used — duplication, checking, and logging — and the convenience-versus-cost trade-off, showing you've worked with real transaction classes.
Common Mistakes
- Giving the mechanism without the pitfall. The intermediate interviewer probes "what happens if?"; pair every mechanism with its failure mode (a missing item_done hangs, an unconnected analysis port silently checks nothing) to show hands-on experience.
- Describing active/passive without the connect-phase guard. Knowing active builds the driver isn't enough; the connection in connect_phase must also be guarded on is_active, or the passive case crashes.
- Forgetting create-not-new for overrides. Saying you register an override without noting the target must be created through the factory misses the most common reason an override silently doesn't take.
- Treating the analysis connection as automatic. The monitor-to-scoreboard connection must be made in connect_phase and confirmed; an unconnected analysis port is a silent green-passing failure.
- Not knowing two analysis inputs need the decl macro. A scoreboard with input and output streams needs uvm_analysis_imp_decl with distinct suffixes; two plain imps collide on write.
- Reciting phases without the objection mechanics. The run phase needs an objection raised or it ends in zero time; knowing the raise-stimulus-drop pattern and its two failure directions is the practical test.
Senior Design Review Notes
Exercises
- Mechanism plus pitfall. For the sequencer-driver handshake, the config-DB virtual-interface flow, and the monitor-scoreboard connection, write the mechanism and the characteristic pitfall for each.
- Diagnose the fault. Write your ordered diagnostic for "the test runs but nothing drives" and for "the scoreboard reports zero comparisons."
- Build the agent. Outline the build_phase and connect_phase of a configurable active/passive agent, noting what is guarded on is_active.
- Wire the scoreboard. Describe how you'd connect a two-input scoreboard, including the decl macro and the connect-phase connections.
Summary
- Intermediate UVM interviews are the practical exam — they test whether you can build and connect a working, reusable environment, probing with "how do you?" and "what happens if?", because the practical mechanics and pitfalls only show up when you've actually built environments.
- The areas: the sequencer-driver handshake (
get_next_item/item_done, blocking, hangs ifitem_doneis forgotten) and sequence layering (sequences calling sequences, virtual sequences coordinating agents via a virtual sequencer); the config DB and factory in practice (the virtual-interface flow, config objects, registering overrides in a test before super.build_phase); TLM and analysis (monitor analysis port to scoreboard export in connect_phase, two inputs viauvm_analysis_imp_decl); phasing and objections (the raise-stimulus-drop pattern, default sequences, scheduled sub-phases); and composing the env (conditional build, guarded connect, field automation). - The meta-skill: answer like a mechanic, not a manual — mechanism, then practical detail, then pitfall-with-diagnostic — because fluency under a real fault is what proves hands-on experience.
- The silent failures are especially distinguishing: an unconnected analysis port (scoreboard passes green checking nothing), a config-DB mismatch (null interface crashing later), a missing objection (zero-time pass) — knowing which failures are silent signals you've been burned by them.
- The durable rule of thumb: answer intermediate UVM questions like a mechanic handed a non-running engine — give the mechanism, the practical detail (the actual call and gotcha), and the pitfall with how you'd diagnose it — because the intermediate level is the practical exam that separates naming the parts from assembling them, and the interviewer's "how do you?" and "what happens if?" probe for the hands-on fluency (the sequencer-driver handshake and its hang, the config-DB flow and its null vif, the analysis connection and its silent failure, the objection pattern and its zero-time finish) that only building and debugging real environments produces.
Next — Advanced Questions: with the practical mechanics established, the next chapter steps up to the advanced level — the questions that probe deep methodology and judgment: reusable verification IP and sequence libraries, advanced factory and configuration patterns, the register abstraction layer, functional coverage and closure, complex multi-agent and SoC-level architecture, and the design trade-offs where there is no single right answer — the questions that distinguish an engineer who can build a testbench from one who can architect a verification methodology.