UVM Monitor built mon_analysis_port and used it to broadcast every reg_transaction it captured - but nothing was listening yet. This article builds the piece that does: a component whose entire job is to receive whatever a monitor sends and do something with it, usually sampling functional coverage.
6 min read | Intermediate Level
What You'll Learn
- What a subscriber is, and how it differs from a plain
uvm_component - The shape of the
uvm_subscriberbase class and itswrite()method - How to connect a monitor's analysis port to a subscriber's
analysis_export - Why subscribers are the natural home for functional coverage
What Is a Subscriber?
A subscriber is a listener on an analysis port. Whenever a broadcaster - almost always a monitor - writes an object to its analysis port, every subscriber connected to that port receives a copy automatically. A plain uvm_component has no built-in way to receive that copy; uvm_subscriber is exactly uvm_component plus one thing: a ready-made analysis export named analysis_export.
The uvm_subscriber Class
Here's the shape of the base class itself:
virtual class uvm_subscriber #(type T=int) extends uvm_component;
typedef uvm_subscriber #(T) this_type;
uvm_analysis_imp #(T, this_type) analysis_export;
function new(string name, uvm_component parent);
super.new(name, parent);
analysis_export = new("analysis_imp", this);
endfunction
pure virtual function void write(T t);
endclass
The type parameter T is whatever transaction this subscriber expects to receive - in this tutorial's case, reg_transaction. analysis_export is already constructed for you inside the base class's own constructor, which is why a subscriber's new() only ever needs to call super.new() and nothing more. The one thing every subscriber must supply is write(), declared pure virtual here, meaning a derived class is required to define it - this is the method that actually runs every time something arrives.
The full mechanics of how an analysis port finds and calls every connected subscriber's write() - including what happens when more than one subscriber is connected to the same port - are covered in UVM TLM Analysis Port. For now, the only thing to know is: connect a port to analysis_export, and write() runs automatically whenever that port broadcasts.
Building reg_coverage
A subscriber built for functional coverage typically does very little beyond sampling a covergroup inside write():
class reg_coverage extends uvm_subscriber #(reg_transaction);
`uvm_component_utils(reg_coverage)
covergroup cg_reg_transaction;
coverpoint trans.addr;
coverpoint trans.wr_en;
endgroup
reg_transaction trans;
function new(string name, uvm_component parent);
super.new(name, parent);
cg_reg_transaction = new();
endfunction
virtual function void write(reg_transaction t);
trans = t;
cg_reg_transaction.sample();
endfunction
endclass
write() is the only method reg_coverage is required to implement, and it does exactly two things: store the incoming transaction where the covergroup's coverpoints can see it, then sample. Everything else - constructing the analysis export, receiving the call in the first place - is handled by uvm_subscriber itself.
This covergroup is deliberately minimal - just enough to show where sampling happens. Designing real coverage models, with bins, cross coverage, and coverage-driven test closure, is a topic of its own.
Connecting the Monitor to the Subscriber
None of this runs until reg_coverage's analysis_export is actually connected to reg_monitor's mon_analysis_port from UVM Monitor. That connection is made once, in whichever component owns both of them - typically an agent or environment - inside its connect_phase:
virtual function void connect_phase(uvm_phase phase);
super.connect_phase(phase);
m_monitor.mon_analysis_port.connect(m_coverage.analysis_export);
endfunction
After this single line runs, every reg_transaction reg_monitor writes to mon_analysis_port arrives at reg_coverage's write() automatically, for the rest of the simulation. Nothing about reg_monitor needs to change to support this - it was already broadcasting; it just had no one listening until now.
Quick Check
Q: If you also wanted a scoreboard to see every transaction reg_monitor captures, in addition to reg_coverage, what would need to change in reg_monitor itself?
Show Answer
Nothing. An analysis port can broadcast to more than one connected subscriber at once - you'd add a second connect() call in connect_phase, wiring the same mon_analysis_port to the scoreboard's own analysis export. The monitor doesn't need to know how many listeners exist, or that a second one was just added.
Why Use uvm_subscriber Instead of Plain uvm_component?
Nothing stops you from adding your own analysis export to a plain uvm_component by hand. The reason to extend uvm_subscriber instead is the same reason to extend uvm_monitor instead of uvm_component for a monitor: it's a uniform, recognizable way for any component to declare "I receive transactions from an analysis port," and the required write() method means there's never any doubt about how data gets in.
Common Mistakes
Mistake #1: Forgetting to implement write()
Since write() is declared pure virtual in uvm_subscriber, a derived class that doesn't define it won't compile. This is intentional - there's no sensible default behavior for "something arrived," so the base class forces every subscriber to decide what to do with it.
Mistake #2: Forgetting the connect_phase wiring
A correctly written reg_coverage with a correctly written write() still does nothing if its analysis_export was never connected to a monitor's analysis port. There's no error or warning for this - the simulation just runs with coverage silently staying at zero, which makes it worth double-checking the connect_phase wiring early if coverage numbers ever look suspiciously empty.
Practice Exercise
Challenge: Add a Read/Write Coverpoint
Extend reg_coverage's covergroup with a coverpoint on rd_en as well as wr_en, so coverage tracks reads and writes as separate events.
Hint
A covergroup can have more than one coverpoint line. Look at how wr_en is already declared as a coverpoint and add a matching line for rd_en.
Solution
covergroup cg_reg_transaction; coverpoint trans.addr; coverpoint trans.wr_en; coverpoint trans.rd_en; endgroup
Each coverpoint line tracks the values a single field takes on across every sampled transaction, independently of the others - adding one for rd_en doesn't change how wr_en or addr are tracked.
Summary
In this article, you learned:
- A subscriber listens on an analysis port and receives a copy of everything broadcast to it
uvm_subscriberisuvm_componentplus a built-inanalysis_exportand a requiredwrite()method- Connecting a monitor's analysis port to a subscriber's
analysis_exporthappens once, inconnect_phase - Subscribers are the natural home for functional coverage, since
write()runs automatically every time a new transaction arrives
Going Deeper
- How an analysis port calls every connected write(): UVM TLM Analysis Port
- Ports, exports and imps: UVM TLM