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

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_logging guard is already there. The next engineer adds enable_power_monitor. The one after that adds enable_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_callbackpost_trigger() fires immediately on every trigger
  • Multiple components share the same named event — zero coupling between them
  • .wait_trigger_data() passes a uvm_object payload 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_cb runs before spurious_cb if added first; order matters for pre_drive but usually not for post_drive
  • `uvm_register_cb must appear inside the component class definition — not in the callback class
  • Using new() instead of type_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 with evt.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 1 from post_trigger() in an event callback consumes the event — subsequent wait_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_callbacks executes in registration order — latency_cb runs before spurious_cb if added first; order matters for pre_drive hooks 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 every irq_driver instance — 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_phase wiring
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_phase and 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

Author
Milan Kubavat
Sharing knowledge about silicon verification, hardware design, and engineering insights.

Comments (0)

Leave a Comment