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.
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[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 observersSubclass 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 .
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[CHECK] lightweight write() pattern
input validation
▼
optional clone
▼
queue or counters
▼
return
heavy compare/report in run_phase or helper tasks[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 effectsTreat 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.
[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 replayclass 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[MON][CHECK] composable chain idea
monitor.ap
├─► coverage_sub
├─► stats_sub
└─► scoreboard_sub
each consumer has one focused responsibilityDecompose 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.
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[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 checkingKey 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.