Observer Pattern in UVM: Reacting to Events with Callbacks and Event Pools
With Structural patterns behind us — Adapter, Decorator, Facade, and Proxy — we move to Behavioral patterns. Where Structural patterns define how objects are composed, Behavioral patterns define how they communicate. Observer is where we start: the most fundamental notification pattern in software.
You have built a clean interrupt controller testbench: an IRQ driver, a CPU agent, a scoreboard, and a coverage collector. The driver works. Now the product team wants latency injection for stress testing. The QA team wants coverage sampling. The debug team wants error logging. Where does all that go?
- The Problem: Tight Coupling Across Testbench Components
- Gang of Four: The Observer Pattern
- UVM's Observer: uvm_callback and uvm_event
- Building the Interrupt Controller Testbench
- Scaling Up: Observer Ordering, Filtering, and Scope Control
- Advanced: Observer + Factory
- Quick Reference and Behavioral Patterns Series Intro
The Problem: Tight Coupling Across Testbench Components
Verification engineers discover this problem gradually. The driver is working. A scoreboard needs to know when an IRQ fires. The fastest fix is a direct call from the driver into the scoreboard. Then coverage needs a sample. Another direct call. Then a logger. Then a latency injector. Each addition feels small. The result is a driver that has quietly become a notification switchboard — responsible for stimulus and for orchestrating every downstream reaction to that stimulus.
The Naive Solution — The Driver Does It All
Here is what the driver looks like after everyone has added their hook:
// BAD: driver directly notifies every downstream observer
task drive_irq(irq_seq_item item);
@(posedge vif.clk);
vif.irq[item.irq_id] <= item.level;
// Driver now knows about everything watching it
scoreboard.record_irq(item);
coverage.sample(item.irq_id, item.level);
if (enable_logging) logger.log_irq(item);
if (inject_latency) #(latency_ns);
endtask
This compiles. It even passes regression — for a while. The problems surface when the next team member arrives with a new requirement.
- The driver has become a notification hub — it violates Single Responsibility. Driving stimulus and managing a subscriber list are two separate jobs.
- Adding a new observer (e.g., a power monitor) requires modifying the driver source. Every change to the driver risks breaking stimulus behavior.
- Test-specific behavior — latency injection, spurious IRQs — is tangled into the driver, making it harder to reuse across projects or hand off to another engineer.
- Disabling one observer means adding more flags and branches. The
enable_loggingguard is already there. The next engineer addsenable_power_monitor. The one after that addsenable_protocol_checker. The driver accumulates switches it was never designed to carry.
What you want is a Subject that fires events without knowing who is listening — and Observers that register themselves without the Subject's knowledge. That is the Observer pattern. In UVM, you already have two implementations of it: uvm_callback and uvm_event_pool.
Gang of Four: The Observer Pattern
Define a one-to-many dependency between objects so that when one object changes state, all its dependents are notified and updated automatically.
classDiagram
class Subject {
<>
+attach(o: Observer)
+detach(o: Observer)
+notify()
}
class Observer {
<>
+update()
}
class ConcreteSubject {
-observers: Observer[]
-state
+attach(o: Observer)
+detach(o: Observer)
+notify()
+getState()
}
class ConcreteObserver {
-subject: ConcreteSubject
+update()
}
Subject <|.. ConcreteSubject
Observer <|.. ConcreteObserver
ConcreteSubject --> Observer : notifies
ConcreteObserver --> ConcreteSubject : observes
The Subject maintains a list of Observers and calls their update() when its state changes. The Subject never knows the concrete type of its Observers — it calls the Observer interface. Observers register and unregister themselves at runtime. Neither side is coupled to the other's implementation.
Both Observer and Mediator manage communication between components, but in opposite directions. Observer is one-to-many push notification — one Subject, many Observers, Subject broadcasts. Mediator is many-to-many coordination — multiple components talk through the mediator, which knows all participants. Observer decouples the Subject from its Observers; Mediator decouples components from each other.
Unlike Structural patterns — which shaped how objects are composed (wrapping, adapting, simplifying, proxying) — Behavioral patterns define how objects communicate at runtime. Observer is where this series begins: the simplest, most direct communication pattern. One side changes state; the other side reacts.
UVM's Observer: uvm_callback and uvm_event
UVM ships with two Observer implementations serving different use cases. uvm_callback is the structurally faithful GoF Observer — components are Subjects, callback objects are Observers, `uvm_do_callbacks is notify(). uvm_event/uvm_event_pool is the lightweight event-driven Observer — a named event pool acts as a shared Subject, .trigger() is notify, .wait_trigger() is the Observer reaction. Same pattern, two granularities.
uvm_callback = Canonical Observer
| GoF Role | UVM Concept |
|---|---|
| Subject | uvm_driver / uvm_monitor component |
| ConcreteSubject | irq_driver — registers the callback type with `uvm_register_cb |
| Observer | uvm_callback-derived class |
| ConcreteObserver | irq_latency_injector, irq_coverage_cb, etc. |
notify() |
`uvm_do_callbacks(COMP, CB, METHOD(args)) |
| Observer registry | uvm_callback_pool — managed per component instance |
- Observers register per-instance:
uvm_callbacks#(irq_driver, irq_callback)::add(drv, cb) - Or type-wide:
uvm_callbacks#(irq_driver, irq_callback)::add_by_name("*", cb, null) - The component never knows the concrete Observer type — pure GoF decoupling
uvm_event / uvm_event_pool = Lightweight Observer
| GoF Role | UVM Concept |
|---|---|
| Subject | uvm_event retrieved from uvm_event_pool::get_global_pool() |
notify() |
evt.trigger() / evt.trigger(data) |
| Observer (synchronous) | Any task calling evt.wait_trigger() or evt.wait_trigger_data(data) |
| Observer (callback) | uvm_event_callback — post_trigger() fires immediately on every trigger |
- Multiple components share the same named event — zero coupling between them
.wait_trigger_data()passes auvm_objectpayload alongside the trigger — the IRQ item itself
When to Use Which
| Mechanism | Use when | Modifies behavior? | Carries data? | Coupling |
|---|---|---|---|---|
| Analysis port (from Decorator post) | Passive observation of transactions | No | Yes (transaction object) | Explicit connect_phase wiring |
uvm_event |
React to a moment in time | No | Optional (uvm_object) |
Zero — shared name only |
uvm_callback |
Inspect or modify component behavior | Yes | Yes (via item reference) | Registered per component instance |
Building the Interrupt Controller Testbench
Part A: uvm_callback on the IRQ Driver
The irq_seq_item is the data object passed between sequences and the driver. Every transaction carried on the IRQ stimulus path is an instance of this class.
class irq_seq_item extends uvm_sequence_item;
rand bit [7:0] irq_id; // Which interrupt line
rand bit level; // 1 = assert, 0 = deassert
`uvm_object_utils_begin(irq_seq_item)
`uvm_field_int(irq_id, UVM_ALL_ON)
`uvm_field_int(level, UVM_ALL_ON)
`uvm_object_utils_end
function new(string name = "irq_seq_item");
super.new(name);
endfunction
endclass
The irq_callback base class is the Observer interface. It declares two virtual hook tasks — pre_drive and post_drive — that concrete Observers override to inject behavior before and after each transaction is driven.
class irq_callback extends uvm_callback;
`uvm_object_utils(irq_callback)
function new(string name = "irq_callback");
super.new(name);
endfunction
// Called BEFORE the driver drives the item -- Observers can modify item
virtual task pre_drive(irq_driver drv, irq_seq_item item);
endtask
// Called AFTER the driver drives the item -- Observers can react
virtual task post_drive(irq_driver drv, irq_seq_item item);
endtask
endclass
The irq_driver is the Subject. The `uvm_register_cb macro makes this component a Subject by associating the callback type with the component class. The `uvm_do_callbacks macro is the notify() call — it walks the registered Observer list and calls the named method on each.
class irq_driver extends uvm_driver#(irq_seq_item);
`uvm_component_utils(irq_driver)
// Register the callback type -- makes this component a Subject
`uvm_register_cb(irq_driver, irq_callback)
virtual irq_if vif;
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 irq_if)::get(this, "", "vif", vif))
`uvm_fatal("CFG", "No irq_if found in config_db")
endfunction
task run_phase(uvm_phase phase);
irq_seq_item item;
forever begin
seq_item_port.get_next_item(item);
// notify() -- pre_drive: Observers can inspect or modify item
`uvm_do_callbacks(irq_driver, irq_callback, pre_drive(this, item))
// Drive the interrupt line
@(posedge vif.clk);
vif.irq[item.irq_id] <= item.level;
// notify() -- post_drive: Observers react to what was driven
`uvm_do_callbacks(irq_driver, irq_callback, post_drive(this, item))
seq_item_port.item_done();
end
endtask
endclass
The three concrete Observers below are ConcreteObservers in GoF terms. Each carries exactly one concern: latency injection, spurious IRQ generation, and coverage sampling. None of them requires any change to the driver.
// Observer 1: Injects latency before each IRQ assertion
class irq_latency_injector extends irq_callback;
rand int unsigned latency_cycles;
constraint c_latency { latency_cycles inside {[1:10]}; }
`uvm_object_utils(irq_latency_injector)
function new(string name = "irq_latency_injector");
super.new(name);
endfunction
virtual task pre_drive(irq_driver drv, irq_seq_item item);
if (item.level) begin // Delay assertions only
repeat(latency_cycles) @(posedge drv.vif.clk);
`uvm_info("LAT_CB", $sformatf("Injected %0d cycle latency on IRQ%0d",
latency_cycles, item.irq_id), UVM_HIGH)
end
endtask
endclass
// Observer 2: Fires a spurious IRQ pulse after each legitimate assertion
class spurious_irq_injector extends irq_callback;
`uvm_object_utils(spurious_irq_injector)
function new(string name = "spurious_irq_injector");
super.new(name);
endfunction
virtual task post_drive(irq_driver drv, irq_seq_item item);
if (item.level && ($urandom_range(0, 3) == 0)) begin // 25% chance
@(posedge drv.vif.clk);
drv.vif.irq[item.irq_id] <= 0;
@(posedge drv.vif.clk);
drv.vif.irq[item.irq_id] <= 1;
`uvm_info("SPUR_CB", $sformatf("Spurious IRQ%0d injected", item.irq_id), UVM_MEDIUM)
end
endtask
endclass
// Observer 3: Samples interrupt coverage
class irq_coverage_cb extends irq_callback;
covergroup irq_cg with function sample(bit [7:0] irq_id, bit level);
cp_id : coverpoint irq_id { bins irq[] = {[0:7]}; }
cp_level : coverpoint level;
cx : cross cp_id, cp_level;
endgroup
`uvm_object_utils(irq_coverage_cb)
function new(string name = "irq_coverage_cb");
super.new(name);
irq_cg = new();
endfunction
virtual task post_drive(irq_driver drv, irq_seq_item item);
irq_cg.sample(item.irq_id, item.level);
endtask
endclass
Tests attach Observers to the driver in connect_phase without modifying the driver source. The driver never changes; the Observer list grows transparently per test.
class irq_stress_test extends base_test;
irq_latency_injector latency_cb;
spurious_irq_injector spurious_cb;
irq_coverage_cb coverage_cb;
function void build_phase(uvm_phase phase);
super.build_phase(phase);
latency_cb = irq_latency_injector::type_id::create("latency_cb");
spurious_cb = spurious_irq_injector::type_id::create("spurious_cb");
coverage_cb = irq_coverage_cb::type_id::create("coverage_cb");
endfunction
function void connect_phase(uvm_phase phase);
irq_driver drv = env.irq_agent.driver;
// Attach Observers -- driver never changes, observers stack transparently
uvm_callbacks#(irq_driver, irq_callback)::add(drv, latency_cb);
uvm_callbacks#(irq_driver, irq_callback)::add(drv, spurious_cb);
uvm_callbacks#(irq_driver, irq_callback)::add(drv, coverage_cb);
endfunction
endclass
Common pitfalls with uvm_callback:
- Forgetting
super.pre_drive()when extending a callback — breaks the callback chain for other Observers registered after yours - Callbacks execute in registration order —
latency_cbruns beforespurious_cbif added first; order matters forpre_drivebut usually not forpost_drive `uvm_register_cbmust appear inside the component class definition — not in the callback class- Using
new()instead oftype_id::create()for callback objects breaks Factory overrides
Part B: uvm_event_pool for Cross-Component Reaction
Events retrieved from uvm_event_pool by name are shared globally — any two components that call get("irq_asserted") receive the exact same event handle. No wiring, no ports, no connect_phase coupling. The same name is the entire contract.
// Anywhere in the testbench -- same pool, same handle
uvm_event irq_asserted_ev = uvm_event_pool::get_global_pool().get("irq_asserted");
uvm_event irq_acked_ev = uvm_event_pool::get_global_pool().get("irq_acked");
The driver fires trigger() after asserting each interrupt. It does not know who is listening — the event pool is the only dependency. The transaction item is passed as the trigger payload so Observers receive data alongside the notification.
task run_phase(uvm_phase phase);
irq_seq_item item;
uvm_event irq_asserted_ev = uvm_event_pool::get_global_pool().get("irq_asserted");
uvm_event irq_acked_ev = uvm_event_pool::get_global_pool().get("irq_acked");
forever begin
seq_item_port.get_next_item(item);
`uvm_do_callbacks(irq_driver, irq_callback, pre_drive(this, item))
@(posedge vif.clk);
vif.irq[item.irq_id] <= item.level;
if (item.level)
irq_asserted_ev.trigger(item); // Broadcast to all waiters, pass item as payload
`uvm_do_callbacks(irq_driver, irq_callback, post_drive(this, item))
seq_item_port.item_done();
end
endtask
The scoreboard reacts by calling wait_trigger_data() in its own run_phase. There is zero coupling to the driver — the scoreboard only knows the event name and the payload type.
task run_phase(uvm_phase phase);
uvm_object obj;
irq_seq_item item;
uvm_event irq_asserted_ev = uvm_event_pool::get_global_pool().get("irq_asserted");
forever begin
irq_asserted_ev.wait_trigger_data(obj);
if (!$cast(item, obj))
`uvm_fatal("CAST", "irq_asserted event data is not irq_seq_item")
irq_log.push_back(item.irq_id);
`uvm_info("SB", $sformatf("IRQ%0d asserted -- logged", item.irq_id), UVM_MEDIUM)
end
endtask
A sequence uses irq_acked_ev for a handshake with the CPU model. Events enable precise timing coordination between sequences and monitors without polling loops or fixed delays.
task body();
irq_seq_item item;
uvm_event irq_acked_ev = uvm_event_pool::get_global_pool().get("irq_acked");
// Assert interrupt
`uvm_do_with(item, { irq_id == 3; level == 1; })
// Wait for CPU to acknowledge -- no polling, no delays
irq_acked_ev.wait_trigger();
`uvm_info("SEQ", "IRQ3 acknowledged by CPU", UVM_MEDIUM)
// Deassert
`uvm_do_with(item, { irq_id == 3; level == 0; })
endtask
A uvm_event_callback is an Observer on the event itself. post_trigger() fires immediately on every trigger — no wait_trigger() needed in its own thread. This is the right place for logging, timestamping, or metric collection that must never miss an event.
class irq_timestamp_cb extends uvm_event_callback;
`uvm_object_utils(irq_timestamp_cb)
function new(string name = "irq_timestamp_cb");
super.new(name);
endfunction
// Called immediately after every trigger -- no wait_trigger needed
virtual function bit post_trigger(uvm_event e, uvm_object data);
`uvm_info("EVT_CB", $sformatf("irq_asserted @ %0t", $time), UVM_MEDIUM)
return 0; // 0 = don't consume; all wait_trigger() callers still wake up
endfunction
endclass
// Register in the environment's connect_phase:
// irq_timestamp_cb ts_cb = irq_timestamp_cb::type_id::create("ts_cb");
// irq_asserted_ev.add_callback(ts_cb);
Common pitfalls with uvm_event_pool:
- Event trigger fires before
wait_trigger()is called — the event fires once and is done; late arrivals miss it. Reset the event withevt.reset()if you need it re-triggerable. - Global pool name typo (
"irq_assrted"vs"irq_asserted") silently creates a second event — neither side communicates. Define event names as package constants. - Returning
1frompost_trigger()in an event callback consumes the event — subsequentwait_trigger()callers in the same delta never wake up.
Scaling Up: Observer Ordering, Filtering, and Scope Control
Callback Ordering and Guards
When multiple Observers are registered on the same component, uvm_callbacks executes them in registration order. Individual Observers can be suspended and resumed without removing them from the list — the component keeps its full Observer roster intact while a specific Observer sits dormant for a targeted window of stimulus.
// Disable latency injection for this burst
latency_cb.callback_mode(0);
burst_seq.start(env.irq_agent.sequencer);
latency_cb.callback_mode(1);
uvm_callbacksexecutes in registration order —latency_cbruns beforespurious_cbif added first; order matters forpre_drivehooks that modify the item before it reaches the next Observer in the chain.callback_mode(0)disables a specific Observer without unregistering it;callback_mode(1)re-enables it. The callback slot remains reserved and the Observer retains all its state across the disable/re-enable window.- This is the right pattern for suppressing latency injection during a critical timing window — the injector stays registered for the full test, quiet only during the burst where its delay would corrupt protocol timing.
- Per-instance vs type-wide registration:
add(drv, cb)attaches an Observer to one specific driver instance, giving precise per-instance control.add_by_name("*", cb, null)applies the Observer to everyirq_driverinstance — use per-instance for targeted fault injection, type-wide for global instrumentation such as coverage or logging that should fire everywhere.
Event-Based Fan-Out
A single uvm_event trigger simultaneously unblocks every component that has called wait_trigger() on that event. There is no queue, no round-robin, no ownership handoff — all waiters wake in the same simulation delta and receive the same payload reference.
// Three components all wait on the same event -- driver fires once, all three wake up
// In scoreboard: irq_asserted_ev.wait_trigger_data(obj);
// In power_mon: irq_asserted_ev.wait_trigger_data(obj);
// In trace_logger: irq_asserted_ev.wait_trigger_data(obj);
// Driver triggers once:
irq_asserted_ev.trigger(item);
// All three tasks unblock simultaneously in the next delta
- Each
wait_trigger_data()caller receives the same payload reference — no object copying, no ownership conflicts. Observers that need to preserve the data must clone it themselves before the next trigger replaces the reference. evt.reset(UVM_DELTA)after a trigger makes the event re-triggerable from the next delta. Use this in always-on monitors that must catch every interrupt across the full simulation — without reset, a second trigger on an already-triggered event has no effect.
Choosing the Right Observer Mechanism
Analysis port → passive observation of transactions, needs explicit
connect_phasewiring
uvm_event → synchronization to moments in time, zero wiring, shared name only
uvm_callback → active modification of component behavior, registered per instance
Three concrete selection rules:
- “I want to count / log / check transactions from a monitor” → analysis port (from the Decorator post): transactions flow out of the monitor through a typed port; subscribers connect in
connect_phaseand receive every transaction without any coupling to the monitor internals. - “I want sequence B to start exactly when sequence A reaches a checkpoint” → uvm_event: sequence A triggers the named event; sequence B calls
wait_trigger()with no connection between the two sequences. The event pool is the only shared dependency. - “I want this test to inject errors into the driver without modifying the driver” → uvm_callback: the test registers a concrete callback on the driver instance in
connect_phase; the driver source stays unchanged across all tests.
Advanced: Observer + Factory
The Factory decides which Observer implementation gets created. The Observer controls what happens when the Subject fires. Together: swap Observer behavior at test time — regression runs get lightweight coverage-only Observers, chaos tests get full error injection — without changing the driver or environment.
The irq_chaos_injector below extends irq_latency_injector, inheriting its pre_drive latency injection while adding spurious glitch injection in post_drive:
// Base Observer: irq_latency_injector (already defined -- registered via Factory)
// irq_latency_injector::type_id::create(...) -- default in base_test
// Extended Observer: latency + spurious glitch injection
class irq_chaos_injector extends irq_latency_injector;
`uvm_object_utils(irq_chaos_injector)
function new(string name = "irq_chaos_injector");
super.new(name);
endfunction
// post_drive adds chaos on top of latency injector's pre_drive
virtual task post_drive(irq_driver drv, irq_seq_item item);
if (item.level && ($urandom_range(0, 1) == 0)) begin
// Random spurious glitch: deassert and reassert the same IRQ line
@(posedge drv.vif.clk);
drv.vif.irq[item.irq_id] <= 0;
repeat($urandom_range(1, 3)) @(posedge drv.vif.clk);
drv.vif.irq[item.irq_id] <= 1;
`uvm_info("CHAOS_CB",
$sformatf("Chaos: spurious glitch on IRQ%0d", item.irq_id), UVM_MEDIUM)
end
endtask
endclass
One line in the test swaps the Observer without touching the driver or environment:
class chaos_test extends base_test;
function void build_phase(uvm_phase phase);
// Swap latency_injector -> chaos_injector before env is built
irq_latency_injector::type_id::set_type_override(
irq_chaos_injector::get_type());
super.build_phase(phase);
endfunction
endclass
// base_test creates the observer normally -- Factory resolves to chaos_injector:
// latency_cb = irq_latency_injector::type_id::create("latency_cb");
// --> actual object is irq_chaos_injector
sequenceDiagram
participant Test as chaos_test
participant Factory
participant Env
participant Driver as irq_driver (Subject)
participant ChaosObs as irq_chaos_injector (Observer)
participant EventPool as uvm_event_pool
participant Scoreboard
Test->>Factory: set_type_override(irq_chaos_injector)
Test->>Env: build_phase()
Env->>Factory: create("latency_cb")
Factory-->>Env: irq_chaos_injector instance
Env->>Driver: uvm_callbacks::add(drv, chaos_cb)
Note over Driver,ChaosObs: Observer registered on Subject
Driver->>ChaosObs: uvm_do_callbacks(pre_drive)
ChaosObs-->>Driver: inject latency cycles
Driver->>Driver: vif.irq[id] <= 1
Driver->>EventPool: irq_asserted.trigger(item)
EventPool-->>Scoreboard: wait_trigger_data unblocks
Scoreboard->>Scoreboard: log IRQ for checking
Driver->>ChaosObs: uvm_do_callbacks(post_drive)
ChaosObs-->>Driver: inject spurious glitch
Factory (what to create), Adapter (interface translation), Decorator (behavior addition), Facade (subsystem simplification), Proxy (access control), Observer (decoupled notification) — six patterns, one testbench. Observer closes the loop: every time a Factory-created component fires uvm_do_callbacks, it’s notifying whoever registered to listen — without knowing who that is.
Quick Reference and Behavioral Patterns Series Intro
uvm_callback API Cheatsheet
| Method/Macro | Purpose |
|---|---|
`uvm_register_cb(COMP, CB) |
Register callback type on a component — goes inside component class definition |
`uvm_do_callbacks(COMP, CB, METHOD(args)) |
Invoke callback method on all registered Observers |
uvm_callbacks#(COMP, CB)::add(inst, cb) |
Register a callback instance on a specific component instance |
uvm_callbacks#(COMP, CB)::add_by_name("*", cb, null) |
Register on all instances of COMP |
uvm_callbacks#(COMP, CB)::delete(inst, cb) |
Unregister a callback instance |
cb.callback_mode(0) / cb.callback_mode(1) |
Disable / re-enable without unregistering |
uvm_event_pool API Cheatsheet
| Method | Purpose |
|---|---|
uvm_event_pool::get_global_pool().get("name") |
Get (or create) a named event — same name = same event |
evt.trigger() |
Notify all wait_trigger() callers; no payload |
evt.trigger(uvm_object data) |
Notify all wait_trigger_data() callers with payload |
evt.wait_trigger() |
Block until event fires |
evt.wait_trigger_data(output uvm_object data) |
Block until event fires; receive payload |
evt.add_callback(uvm_event_callback cb) |
Register an event callback Observer |
evt.delete_callback(cb) |
Unregister event callback |
evt.reset() / evt.reset(UVM_DELTA) |
Make event re-triggerable |
Common Mistakes
| Mistake | Fix |
|---|---|
Forgetting super.pre_drive()/super.post_drive() in callback |
Always call super in overridden hooks — other chained Observers depend on it |
Event trigger fires before wait_trigger() is called |
Ensure waiters are active before trigger; use evt.reset() for re-triggerable events |
| Global pool name typo creates a second silent event | Define all event names as package constants: parameter string IRQ_ASSERTED = "irq_asserted" |
post_trigger() returns 1 — consumes the event |
Return 0 from event callbacks unless you explicitly want to block other waiters |
Using new() instead of type_id::create() for callback objects |
Always use create() so Factory overrides work |
| Registering callback type-wide when only one driver should be affected | Use add(inst, cb) with the specific driver instance |
Analysis Port vs uvm_event vs uvm_callback
| Analysis Port | uvm_event | uvm_callback | |
|---|---|---|---|
| GoF pattern | Observer (passive) | Observer (reactive) | Observer (canonical) |
| Coupling | Explicit connect_phase wiring | Zero — shared name only | Registered per component instance |
| Modifies behavior | No | No | Yes — callbacks can change the item or inject delays |
| Carries data | Yes — full transaction object | Optional uvm_object | Yes — via item reference in method args |
| When to use | Monitoring/checking transactions from a monitor | Cross-component synchronization to time events | Test-specific driver/monitor behavior injection |
What’s Next: Behavioral Patterns
Behavioral patterns define how objects communicate. We’ve covered how to build objects (Creational: Factory, Singleton, Builder, Prototype) and how to compose them (Structural: Adapter, Decorator, Facade, Proxy) — now we look at how they talk to each other. Observer is where this series begins: one object changes state, and everything watching it reacts — without anyone in the chain knowing the others’ names.
Previous: Proxy Pattern — Controlling access with register models and sequencers
Next: Strategy Pattern — Protocol-agnostic traffic with adapters and strategy classes
Comments (0)
Leave a Comment