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_subscriber base class and its write() 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_subscriber is uvm_component plus a built-in analysis_export and a required write() method
  • Connecting a monitor's analysis port to a subscriber's analysis_export happens once, in connect_phase
  • Subscribers are the natural home for functional coverage, since write() runs automatically every time a new transaction arrives

Going Deeper

Did you find what you were looking for?

Quiz

Test Your Knowledge

1 of 5 Score: 0 / 5