AMBA APB · Module 15
The UVM APB Agent
The standard reusable UVM APB agent — how it packages a sequencer, driver, and monitor (plus a sequence library and config) into one configurable unit instantiated once per interface. Active mode builds all three and drives stimulus through the get_next_item / item_done handshake; passive mode builds only the monitor. The agent's analysis port feeds the scoreboard and coverage.
The monitor, the driver, and the sequencer are not three loose components you wire by hand on every project — the UVM agent packages them into one reusable, configurable unit that is the standard atom of APB verification. The single idea to carry: the APB agent is a uvm_agent container that, in ACTIVE mode, builds a sequencer, a driver, and a monitor and drives stimulus onto the bus through the sequencer/driver get_next_item/item_done handshake; in PASSIVE mode, builds only the monitor and merely observes — and either way exposes one analysis port that broadcasts every reconstructed transfer to the rest of the environment. You already know the monitor as a standalone observer; here it becomes a sub-component. You already know APB legality; here you learn the component that drives legal APB and the component that arbitrates what to drive. Get the agent right — its build-time construction gated on is_active, its seq_item_port/seq_item_export connection, its exposed analysis port — and you have a single configurable block you instantiate once per bus and reuse across every project. Get it wrong — a passive agent that still builds a driver, a driver that never calls item_done — and the environment either drives a bus it should only watch, or hangs forever.
1. Problem statement
The problem is packaging stimulus generation and observation for one APB interface into a single, reusable, configurable component — so that any environment can instantiate one agent per bus, switch it between driving and observing with a single config knob, and consume its transactions through one analysis port — without re-wiring a sequencer, driver, and monitor by hand each time.
A verification environment for an APB peripheral needs three capabilities at the interface: something to decide what stimulus to apply (sequences), something to translate that stimulus into pin wiggles in protocol order (the driver), and something to witness what actually happened and report it as transactions (the monitor). Building those as three independently-instantiated components in every testbench is repetitive, error-prone, and unreusable. The agent solves this with three hard requirements:
- It must be a single container with a clean external contract. The rest of the environment should see one component per interface, configure it, connect to its one analysis port, and run sequences on its one sequencer — not reach inside to wire sub-components. The agent's internal topology is its own business; its external surface is an analysis port out and a sequencer to target.
- It must be configurable between active and passive at build time. The same agent class must be able to drive an interface in one testbench (block-level: stimulate the DUT) and only observe an identical interface in another (integration-level: watch a real master driving the DUT). That choice is
is_active—UVM_ACTIVEbuilds sequencer+driver+monitor,UVM_PASSIVEbuilds the monitor alone — and it must be set from outside viauvm_config_db, decided atbuild_phase, never hard-coded in the components. - It must drive legal APB through a disciplined sequencer/driver handshake. When active, the driver pulls one transaction at a time from the sequencer with
get_next_item, drives it through SETUP then ACCESS, waits forPREADYto know the slave is done, capturesPRDATA/PSLVERR, and returns the result withitem_done— exactly oneitem_doneperget_next_item, or the sequencer/driver pipeline stalls.
So the job is not "know APB" or "know the monitor" — you do. It is to package the interface's stimulus and observation into one configurable unit with a disciplined internal handshake and a single clean external seam.
2. Why previous knowledge is insufficient
You have the pieces but not the package. Chapter 15.3 gave you the monitor as a standalone passive observer; 15.1 gave you the rules the driver must obey when it drives. Neither tells you how the components are assembled, configured, and connected into the reusable unit the environment actually instantiates:
- The monitor is a sub-component here, not the whole story. In 15.3 the monitor was studied alone — its sampling discipline, its two-edge capture, its analysis port. In the agent it is one of up to three sub-components, built unconditionally in both modes, and its analysis port is re-exported as the agent's own port. This chapter does not re-derive the monitor's internals; it shows where the monitor sits inside the container and how the agent surfaces it. For everything about how the monitor samples, refer back to 15.3.
- You know legal APB, but not the component that generates it. 15.1 told you SETUP→ACCESS, address stability,
PREADYcompletion,PSLVERRat completion. The driver is the component whoserun_phaseproduces exactly that pin sequence, and it does so by pulling transactions from a sequencer throughget_next_item/item_done. That handshake — and the sequencer that arbitrates competing sequences onto the driver — is entirely new: it is the mechanism that turns a transaction-level sequence into a protocol-legal pin trace. The driver's legality is policed by the assertions of 15.2; here you build the driver that must pass them. - The analysis port had no consumers in 15.3; now the agent is the producer for the env. The monitor broadcasts; the agent re-exposes that broadcast as its own port, and that port is what the scoreboard (15.4) and the functional-coverage collector (15.5) subscribe to. The agent is the seam between the interface and the rest of the environment — the unit the env top connects to. Understanding that fan-out is what makes the agent a system component rather than a bag of parts.
So the model to add is the configurable container: the uvm_agent that constructs sequencer/driver/monitor (or just the monitor) at build time, wires the sequencer to the driver internally, exposes one analysis port outward, and is the reusable unit the environment instantiates once per APB bus.
3. Mental model
The model: the agent is a self-contained workstation for one bus. Picture a single bench bolted to one APB interface. On it sit up to three workers. The sequencer is a dispatcher holding a queue of work orders (transactions) and deciding which order goes next when several sequences compete. The driver is the hands — it takes one order from the dispatcher, performs it on the bus in the protocol-correct sequence (SETUP, then ACCESS, then wait for the slave's "done"), reports the result back, and asks for the next. The monitor is the silent observer at the same bench, watching the pins and writing a transcript of everything that happens — and it is there whether or not the hands are. A single switch on the bench — is_active — decides whether the workstation is staffed to drive (dispatcher + hands + observer) or staffed only to watch (observer alone). Either way, the bench has exactly one outgoing chute — the analysis port — down which every transcript slides to whoever is listening.
Three refinements make it precise and interview-ready:
- The sequencer arbitrates; the sequence decides; the driver executes. A common confusion is that "the sequencer generates stimulus." It does not — a sequence (a
uvm_sequence) generates transactions and sends them to the sequencer, which arbitrates between sequences and hands one transaction at a time to the driver. The driver never knows which sequence produced the item; it just executes whatever the sequencer gives it. Three distinct roles: sequence = author, sequencer = dispatcher, driver = executor. - The driver/sequencer handshake is a strict request/response loop. The driver's
run_phaseisforever: get_next_item(req) → drive req on the pins → wait for PREADY → write back results → item_done.get_next_itemblocks until a transaction is available and holds the sequencer's arbitration untilitem_doneis called. Exactly oneitem_donemust answer eachget_next_item; miss it and the sequencer never releases, so the driver hangs on its nextget_next_itemand the whole agent stalls. (get_next_item/item_doneis the blocking-arbitration pair;try_next_item/item_doneis its non-blocking sibling for idle-driving drivers.) is_activeis a build-time decision read from config, not a runtime branch in the driver. Atbuild_phase, the agent readsis_activefromuvm_config_db(or its field) and constructs the sequencer and driver only whenUVM_ACTIVE. A passive agent does not build them at all — there is no dormant driver waiting to be enabled. The monitor is constructed unconditionally. This is why "a passive agent still needs a driver" is wrong: in passive mode the driver object never exists.
4. Real SoC implementation
In a real environment the agent is a uvm_agent subclass that constructs its sub-components in build_phase gated on is_active, wires the sequencer to the driver in connect_phase, and re-exports the monitor's analysis port as its own. The driver's run_phase is the protocol-legal request/response loop. Idiomatic and commented:
// apb_agent.sv — the reusable, configurable APB agent.
// ACTIVE: builds sequencer + driver + monitor and drives stimulus.
// PASSIVE: builds only the monitor and observes.
class apb_agent extends uvm_agent;
`uvm_component_utils(apb_agent)
// is_active is inherited from uvm_agent and defaults to UVM_ACTIVE.
// It is set from outside via uvm_config_db at build time (see env below).
apb_sequencer sequencer; // arbitrates sequences onto the driver (active only)
apb_driver driver; // drives pins, runs the SETUP/ACCESS handshake (active only)
apb_monitor monitor; // passive observer — ALWAYS built, both modes
// The agent re-exports the monitor's analysis port as its own external seam.
uvm_analysis_port #(apb_seq_item) ap;
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction
function void build_phase(uvm_phase phase);
super.build_phase(phase);
ap = new("ap", this);
// Monitor is built unconditionally — observation happens in both modes.
monitor = apb_monitor::type_id::create("monitor", this);
// Sequencer + driver are built ONLY when active. In passive mode they
// do not exist at all — there is no dormant driver to enable later.
if (get_is_active() == UVM_ACTIVE) begin
sequencer = apb_sequencer::type_id::create("sequencer", this);
driver = apb_driver ::type_id::create("driver", this);
end
endfunction
function void connect_phase(uvm_phase phase);
super.connect_phase(phase);
// Expose the monitor's broadcast as the agent's analysis port (both modes).
monitor.ap.connect(this.ap);
// Wire the sequencer to the driver ONLY when active: the driver's
// seq_item_port pulls items from the sequencer's seq_item_export.
if (get_is_active() == UVM_ACTIVE)
driver.seq_item_port.connect(sequencer.seq_item_export);
endfunction
endclass
// apb_driver.sv — turns transactions into protocol-legal APB pin activity.
class apb_driver extends uvm_driver #(apb_seq_item);
`uvm_component_utils(apb_driver)
virtual apb_if vif; // from config_db
function new(string name, uvm_component parent); super.new(name, parent); endfunction
function void build_phase(uvm_phase phase);
super.build_phase(phase);
if (!uvm_config_db#(virtual apb_if)::get(this, "", "vif", vif))
`uvm_fatal(get_type_name(), "no virtual interface set for driver")
endfunction
task run_phase(uvm_phase phase);
reset_pins(); // park the bus idle out of reset
forever begin
// ---- 1. REQUEST: block until the sequencer hands us one transaction ----
seq_item_port.get_next_item(req); // holds arbitration until item_done
// ---- 2. SETUP phase: assert PSEL, drive address/control, PENABLE low ----
@(vif.drv_cb);
vif.drv_cb.psel <= 1'b1;
vif.drv_cb.penable <= 1'b0; // SETUP: PENABLE deasserted
vif.drv_cb.paddr <= req.addr;
vif.drv_cb.pwrite <= req.is_write;
if (req.is_write) begin
vif.drv_cb.pwdata <= req.data;
vif.drv_cb.pstrb <= req.strb;
end
// ---- 3. ACCESS phase: assert PENABLE, then HOLD until PREADY is high ----
@(vif.drv_cb);
vif.drv_cb.penable <= 1'b1; // ACCESS begins
// Sample PREADY through the clocking block; wait out any wait states.
// Skipping this wait would race a wait-stated slave (see beat 7).
do @(vif.drv_cb); while (!vif.drv_cb.pready);
// ---- 4. COMPLETION: capture results at the completing edge ----
if (!req.is_write) req.data = vif.drv_cb.prdata; // read data valid only now
req.slverr = vif.drv_cb.pslverr; // PSLVERR valid only now
// ---- 5. De-assert and return to idle ----
@(vif.drv_cb);
vif.drv_cb.psel <= 1'b0;
vif.drv_cb.penable <= 1'b0;
// ---- 6. RESPONSE: exactly ONE item_done per get_next_item ----
seq_item_port.item_done(); // release arbitration; rsp fields filled in req
end
endtask
endclassTwo facts make this the right shape. First, construction is gated on is_active and the monitor is unconditional — get_is_active() == UVM_ACTIVE guards the sequencer and driver create calls and the seq_item_port/seq_item_export connect, while monitor is built and its port re-exported in every mode. That single guard is what makes one class serve both a stimulus-driving block testbench and an observe-only integration testbench. Second, the driver's loop maps one-to-one onto APB phases and is bracketed by get_next_item/item_done — SETUP drives address/control with PENABLE low, ACCESS asserts PENABLE and then waits on PREADY (the do…while absorbs wait states so the driver never races a slow slave), completion captures PRDATA/PSLVERR, and the single item_done answers the single get_next_item so the sequencer releases arbitration for the next transaction. The legality of those pin moves is what the assertions of 15.2 police; the agent's job is to produce them.
5. Engineering tradeoffs
The agent's design is a set of decisions about what gets built, who drives, and what each sub-component connects to. The first table is the active-versus-passive contract — the single most consequential configuration choice:
| Aspect | ACTIVE agent (UVM_ACTIVE) | PASSIVE agent (UVM_PASSIVE) |
|---|---|---|
| Sequencer built? | Yes | No (object never created) |
| Driver built? | Yes | No (object never created) |
| Monitor built? | Yes | Yes (always) |
| Drives the bus? | Yes — runs sequences | No — observes only |
| Sequencer↔driver wired? | Yes (seq_item_port↔seq_item_export) | N/A (no driver) |
| Analysis port exposed? | Yes | Yes |
| Typical use | Block-level: stimulate the DUT slave | Integration: watch a real master drive the DUT |
The second table is the sub-component contract — each part's role and what it connects to:
| Sub-component | Role | Built in | Connects to |
|---|---|---|---|
| Sequencer | Arbitrates competing sequences; hands one item at a time to the driver | Active only | Driver's seq_item_port (via seq_item_export); sequences run on it |
| Driver | Pulls items via get_next_item; drives SETUP/ACCESS; waits PREADY; returns via item_done | Active only | Sequencer's seq_item_export; the DUT pins (drives) |
| Monitor | Passively reconstructs each transfer into an apb_seq_item (see 15.3) | Always | The DUT pins (reads); its ap → agent's ap |
Agent ap | Single external analysis-port seam re-exporting the monitor's broadcast | Always | Scoreboard (15.4) and coverage (15.5) |
The throughline: the monitor and the analysis port are mode-independent and always present; the sequencer and driver are the part that appears only in active mode. The most consequential single decision is making is_active a build-time config knob rather than a hard-coded choice — that is what lets one agent class be reused as a driver in a block testbench and an observer in an integration testbench. The second is keeping the analysis port the only external seam, so the environment connects to the agent, never to its internals.
6. Common RTL mistakes
7. Debugging scenario
The signature agent failure is a driver/sequencer handshake imbalance: a get_next_item without a matching item_done (or the driver completing a transfer before sampling PREADY), so the sequencer never releases arbitration and the simulation hangs — or worse, transactions silently race a wait-stated slave and post wrong data.
- Observed symptom: the test starts, the first one or two transactions drive correctly, then the simulation hangs — no further bus activity, the sequence never advances past its second
start_item/finish_item, and the run eventually times out with the driver blocked. No assertion fires; nothing reports an error. In a variant, the test does not hang but the scoreboard reports read-data mismatches that correlate with wait-stated transfers. - Waveform clue: in the hang case, the bus shows exactly one completed transfer and then
PSELstuck high (or parked) with no new SETUP — the driver is blocked insideget_next_itemwaiting for an item the sequencer will not release because the previous one was never acknowledged. In the data-mismatch variant, the waveform shows the driver de-assertingPENABLEon the first ACCESS cycle even thoughPREADYwas low — it completed the transfer during a wait state, samplingPRDATAone edge before the slave drove it. - Root cause: the driver's
run_phaseeither returned to the top of theforeverloop without callingitem_done(soget_next_itemon the next iteration blocks forever — the sequencer is still holding arbitration for the un-acknowledged item), or it drove the ACCESS phase and immediately captured/completed without thedo…whilePREADY wait, racing a slave that inserts wait states. Both are handshake-discipline bugs: one breaks the request/response count, the other breaks the request/response timing. - Correct RTL (corrected driver loop): bracket every transfer with exactly one
get_next_item/item_done, and never complete ACCESS untilPREADYis sampled high:
task run_phase(uvm_phase phase);
forever begin
seq_item_port.get_next_item(req); // exactly one get per transfer
drive_setup(req); // SETUP: PSEL=1, PENABLE=0, addr/ctrl
@(vif.drv_cb); vif.drv_cb.penable <= 1'b1; // ACCESS begins
do @(vif.drv_cb); while (!vif.drv_cb.pready); // HOLD until slave is ready
if (!req.is_write) req.data = vif.drv_cb.prdata; // capture only at completion
req.slverr = vif.drv_cb.pslverr;
idle_bus();
seq_item_port.item_done(); // exactly one done per get — release
end
endtask- Verification assertion: beyond fixing the loop, add a handshake-balance check and a protocol check. A simulation-level assertion (or a UVM
uvm_objection/heartbeat) that flags ifget_next_itemcount diverges fromitem_donecount catches the count bug; and the APB ACCESS-completion assertion from 15.2 —PENABLEmust remain high untilPREADYis sampled high — catches the timing bug at the pins, turning a silent data race into a loud failure. - Debug habit: when an agent hangs, suspect the handshake before the bus — count
get_next_itemagainstitem_doneand look for the driver blocked on aget. When data is wrong on wait-stated transfers, suspect thePREADYwait — confirm the driver holds ACCESS untilPREADYis high before capturing. A hang is almost always a missing/doubleitem_done; a wait-state-correlated mismatch is almost always a missingPREADYwait. Both live in the driver loop, not the DUT.
8. Verification perspective
The agent is the reusable unit the whole environment is built from, so its reuse story — the same class serving as a stimulus driver in one testbench and a silent observer in another — is itself a verification deliverable, and the analysis-port fan-out is the seam that makes that reuse pay off.
- Passive reuse is the agent's highest-value property at integration. A block-level testbench instantiates the APB agent active to drive the DUT slave directly. When that block is integrated into a subsystem where a real master (another DUT, or a different agent) drives the same bus, you reuse the identical agent class — just configured
passiveviauvm_config_db— to observe the real traffic, reconstruct transactions, and feed the same scoreboard and coverage you used at block level. Nothing about the monitor changes; only the build-timeis_activeflips. This is whyis_activeis config-driven, not hard-coded: it is the switch that lets one verified agent move from block to integration without a rewrite. - The analysis port fans out to scoreboard and coverage independently. The agent exposes one
ap; the env connects it to both the scoreboard (15.4) and the coverage collector (15.5) — each subscribes its own export, and each receives an independent copy of every reconstructed transfer. The agent neither knows nor cares how many subscribers exist; adding a logger or a second coverage model is a connect-time edit in the env, not a change to the agent. This decoupling is what keeps the agent a stable, reusable component while the env around it evolves. uvm_config_dbdrives the whole topology, including active/passive and the virtual interface. Two things are configured into the agent at build: the virtual interface handle (so the driver and monitor know which physical bus to sample/drive) andis_active. Both come throughuvm_config_db::setfrom the env or test, keyed to the agent's path. Getting the config wiring right is a verification concern in itself — a mis-set vif gives auvm_fatal(loud, easy), but a mis-setis_active(passive when you meant active) gives an agent that silently never drives (quiet, dangerous), so the test must assert that an active agent actually produced traffic.- A virtual sequencer coordinates multiple agents when one bus is not enough. When a testbench has several APB agents (e.g. one per interface) or mixes APB with another protocol, a virtual sequencer holds handles to each agent's sequencer and runs a virtual sequence that orchestrates stimulus across them — e.g. "configure peripheral A, then issue a transfer on bus B." The individual agents stay reusable and protocol-local; the cross-bus intent lives in the virtual sequence, not inside any agent. This keeps each agent a clean, single-interface unit while still allowing coordinated multi-agent scenarios.
The point: you exploit the agent by reusing it passive at integration, fanning its one analysis port out to scoreboard and coverage, driving its topology (active/passive + vif) from uvm_config_db, and coordinating multiple agents with a virtual sequencer — because the agent's whole value is being the one configurable, reusable atom of APB verification that every testbench instantiates and connects to identically.
9. Interview discussion
"Walk me through a UVM agent for APB" is a critical verification question, and the answer that signals real experience moves from container to active/passive to the driver handshake.
Lead with the container: the agent is a uvm_agent that packages a sequencer, a driver, and a monitor for one interface, built in build_phase and wired in connect_phase, exposing one analysis port outward. Then the configuration axis: is_active (read from uvm_config_db at build) decides the topology — UVM_ACTIVE constructs sequencer + driver + monitor and drives stimulus; UVM_PASSIVE constructs only the monitor and observes — and the monitor is built unconditionally in both. Make the handshake explicit, because it is where depth shows: the driver's run_phase is forever: get_next_item(req) → drive SETUP then ACCESS → wait on PREADY → capture PRDATA/PSLVERR → item_done, with exactly one item_done per get_next_item or the sequencer never releases arbitration and the agent hangs. Then separate the three roles cleanly: the sequence authors transactions, the sequencer arbitrates between competing sequences, the driver executes — the sequencer does not generate stimulus. The depth flourishes that distinguish a strong answer: the connect-phase wiring — driver.seq_item_port.connect(sequencer.seq_item_export) for stimulus and monitor.ap.connect(agent.ap) to re-export the broadcast; passive reuse — the same agent class drives a DUT at block level and observes a real master at integration with only is_active flipped; and the killer closer: "a hang is almost always a missing or doubled item_done, and a wait-state-correlated data mismatch is almost always a missing PREADY wait in the driver — both are handshake-discipline bugs in the agent, not the DUT." That last line shows you have actually debugged a live agent, not just memorised its diagram.
10. Practice
- Gate the build. Write the
build_phasethat constructs the monitor unconditionally and the sequencer/driver only whenget_is_active() == UVM_ACTIVE, and theconnect_phasethat wires them. State what is absent in passive mode. - Trace the handshake. For one write transfer with two wait states, list each step of the driver loop from
get_next_itemtoitem_done, naming which signal moves on which edge and where thePREADYwait sits. - Active vs passive. Given a block testbench that drives the DUT and an integration testbench that watches a real master, say how each instantiates the same agent class and which sub-components exist in each.
- Config the topology. Write the
uvm_config_db::setcalls from the env that give one agent its virtual interface and make it active, and a second agent its interface and make it passive. Explain why a mis-setis_activeis more dangerous than a mis-set vif. - Sequencer vs sequence. Explain, with one sentence each, the distinct roles of the sequence, the sequencer, and the driver — and why "the sequencer generates stimulus" is wrong.
11. Q&A
12. Key takeaways
- The APB agent is a
uvm_agentcontainer that packages a sequencer, a driver, and a monitor for one interface — the reusable, configurable atom of APB verification, instantiated once per bus and connected to the env through one analysis port. is_activedecides the topology at build time:UVM_ACTIVEbuilds sequencer + driver + monitor and drives stimulus;UVM_PASSIVEbuilds only the monitor and observes — the monitor is always built, the driver and sequencer only in active mode, and the choice comes fromuvm_config_db, never hard-coded.- The driver/sequencer handshake is a strict request/response loop —
get_next_item → drive SETUP/ACCESS → wait PREADY → item_done, with exactly oneitem_doneperget_next_item; miss it and the agent hangs, skip thePREADYwait and it races a wait-stated slave. - Three distinct roles: the sequence authors transactions, the sequencer arbitrates between competing sequences, the driver executes — the sequencer does not generate stimulus, and the monitor (see 15.3) is a passive sub-component independent of the driver.
- The agent re-exports the monitor's analysis port as its single external seam —
monitor.ap.connect(agent.ap)— and the env fans that one port out to the scoreboard (15.4) and coverage (15.5), each an independent subscriber. - Passive reuse is the payoff: the same agent class drives a DUT at block level and observes a real master at integration with only
is_activeflipped — so its block-level scoreboard and coverage carry into the subsystem unchanged, while a virtual sequencer coordinates multiple agents when one bus is not enough.