Part 4 · TLM & Analysis · Intermediate

uvm_subscriber Base Class: Extending Subscriber and Overriding write()

How uvm_subscriber provides analysis_export convenience, common subclass patterns, and robust write() implementations for scoreboarding and coverage.

What uvm_subscriber gives you

uvm_subscriber is a convenience base class for analysis consumers. It already includes an analysis_export and expects you to implement write(T t) with consumer-specific behavior.

Use it when a component's primary job is to consume analysis transactions: functional coverage, protocol audit counters, lightweight projections, or event streams for scoreboards.

systemverilog
class apb_cov_sub extends uvm_subscriber #(apb_txn);
  `uvm_component_utils(apb_cov_sub)

  covergroup cg;
    cp_kind : coverpoint sample_kind;
    cp_addr : coverpoint sample_addr[7:0];
  endgroup

  apb_txn sample_tr;
  int unsigned sample_kind;
  bit [31:0] sample_addr;

  function new(string name, uvm_component parent);
    super.new(name, parent);
    cg = new();
  endfunction

  function void write(apb_txn t);
    sample_tr = t;
    sample_kind = t.kind;
    sample_addr = t.addr;
    cg.sample();
  endfunction
endclass
diagram
[TLM] uvm_subscriber structure

 uvm_subscriber#(T)
   ├─ analysis_export (already present)
   ├─ user fields/state
   └─ write(T t) override (required behavior)

 often ideal for coverage and stream observers
  • Subclass once, implement write(), connect monitor.ap to analysis_export.

  • Good fit for passive analytics and simple stream-derived metrics.

  • Avoid embedding command/control responsibilities in subscriber classes.


write() design principles for subscribers

write() is ingestion, not full test execution. Keep it focused on receiving the sample, optionally cloning, and recording enough information for later analysis threads.

A robust write() often does three things: validate shape , record/queue , and return quickly .

systemverilog
function void write(apb_txn t);
  if (t == null) begin
    `uvm_warning("NULL_TR", "subscriber got null transaction")
    return;
  end

  // clone if downstream logic mutates or stores long-term
  fifo.push_back(t.clone());
endfunction
diagram
[CHECK] lightweight write() pattern

  input validation
     ▼
  optional clone
     ▼
  queue or counters
     ▼
  return

 heavy compare/report in run_phase or helper tasks
diagram
[MON][TLM] ingestion anti-pattern

 write():
   - waits on event
   - loops on giant database search
   - performs expensive string formatting each sample

 result:
   poor runtime and hard-to-debug timing side effects
  • Treat write() as a hot path executed for every observed transaction.

  • Queue first, compare/process later when complexity grows.

  • Warn and return on malformed samples instead of crashing blindly.


Subscriber flavors: coverage, statistics, and pre-check normalization

Not every subscriber needs to be a full scoreboard. You can split responsibilities into specialized subscribers to keep each one simple and composable.

diagram
[CHECK] common subscriber roles

  coverage subscriber:
    - sample cross bins from observed stream

  stats subscriber:
    - count opcode/address/latency classes

  normalizer subscriber:
    - clone + canonicalize fields
    - forward to downstream checker path

  trace subscriber:
    - structured logging for debug replay
systemverilog
class apb_stats_sub extends uvm_subscriber #(apb_txn);
  `uvm_component_utils(apb_stats_sub)
  int unsigned writes, reads, errs;

  function void write(apb_txn t);
    if (t.kind == APB_WRITE) writes++;
    else if (t.kind == APB_READ) reads++;
    if (t.err) errs++;
  endfunction
endclass
diagram
[MON][CHECK] composable chain idea

 monitor.ap
   ├─► coverage_sub
   ├─► stats_sub
   └─► scoreboard_sub

 each consumer has one focused responsibility
  • Decompose by responsibility instead of one monolithic subscriber.

  • Specialized subscribers improve reuse and isolate logic changes.

  • Common monitor stream can drive many independent analysis consumers.


Walkthrough: building a safe subscriber in an env

This example wires monitor analysis output into a subscriber-based coverage collector and a conventional scoreboard, showing mixed consumer styles in one environment.

systemverilog
class apb_env extends uvm_env;
  `uvm_component_utils(apb_env)
  apb_agent      agt;
  apb_cov_sub    cov_sub;
  apb_scoreboard sb;

  function void build_phase(uvm_phase phase);
    super.build_phase(phase);
    agt     = apb_agent::type_id::create("agt", this);
    cov_sub = apb_cov_sub::type_id::create("cov_sub", this);
    sb      = apb_scoreboard::type_id::create("sb", this);
  endfunction

  function void connect_phase(uvm_phase phase);
    agt.mon.ap.connect(cov_sub.analysis_export);
    agt.mon.ap.connect(sb.in);
  endfunction
endclass
diagram
[MON][TLM][CHECK] mixed-consumer wiring

 [MON] agt.mon.ap
   ├─► [CHECK] cov_sub.analysis_export
   └─► [CHECK] sb.in (analysis_imp)

 same observed transaction stream powers coverage and checking

Key takeaways

  • uvm_subscriber is the simplest path to build robust analysis consumers.

  • Implement write() as fast ingestion; defer expensive work off the hot path.

  • Split consumers by role to keep logic maintainable and composable.

  • Connect monitor.ap to subscriber.analysis_export for clean passive observability.

Common pitfalls

  • Using subscriber write() for long-running or blocking workflows.

  • Building all analytics in one mega-subscriber with tangled concerns.

  • Skipping null/shape checks and letting bad samples poison state.

  • Assuming uvm_subscriber replaces all analysis_imp use cases.