Skip to content

UVM

Covergroups

The mechanism that measures coverage — the covergroup: how it is declared, instantiated, and sampled to record which scenarios occurred; the sample function that passes the transaction; where it lives (a coverage subscriber on the monitor's analysis port); and how it turns the coverage plan into a measurable artifact.

Functional Coverage · Module 19 · Page 19.2

The Engineering Problem

The previous chapter argued why coverage exists — to measure completeness, the question checking leaves blank (Module 19.1). But a philosophy is not a measurement: something has to actually record which scenarios the design saw, accumulate it across the run, and report a number. That something is the covergroup — SystemVerilog's concrete construct for functional coverage. Without it, the coverage plan is just a document; with it, the plan becomes a living artifact that watches the transactions and tallies what occurred. But a covergroup is not automatic the way code coverage is — it must be declared (its structure), instantiated (a live collector), and sampled at the right moment (triggered to record the current values). Each step has a failure mode: an un-instantiated covergroup records nothing; an un-sampled one reads 0% though the scenarios ran; a mistimed sample records garbage. The problem this chapter solves is the mechanism: how a covergroup is built and triggered to turn the coverage plan into a measurable artifact.

A covergroup is a SystemVerilog construct that groups a set of coverpoints (and crosses) and is sampled to record the current values into their bins — the concrete instrument that implements the coverage plan. It has a three-step lifecycle: declare (covergroup ... endgroup — defines the structure: which coverpoints, which crosses), instantiate (cg = new() — creates a live collector; covergroups are not auto-constructed), and sample (cg.sample(txn) or an event trigger — records the current values into the bins). The idiomatic UVM form uses a sample function (covergroup cg with function sample(bus_txn t)) so the covergroup samples a transaction object and its coverpoints measure t's fields. It lives in a coverage collector — typically a uvm_subscriber connected to the monitor's analysis port (Module 13.4 / 17.2) — whose write() calls cg.sample(t) on each observed transaction. The covergroup accumulates coverage across the whole simulation (and merges across runs in the coverage database), reporting the percentage that measures completeness. This chapter is the covergroup: its structure, its declare/instantiate/sample lifecycle, the sample trigger, where it lives, and how it becomes the measurable artifact.

What is a covergroup — how is it declared, instantiated, and sampled to record which scenarios occurred, what is the sample function that passes the transaction, where does it live in a UVM environment, and how does it turn the coverage plan into a measurable artifact?

Motivation — why the covergroup is the artifact that makes coverage real

The coverage philosophy (19.1) needs a mechanism to become a measurement. The covergroup is that mechanism, and its design matters:

  • It turns a plan into a living measurement. The coverage plan names the scenarios to exercise; the covergroup is the code that watches the transactions and records which scenarios occurred. Without it, the plan is a document, not a number.
  • It is not automatic — it must be built and triggered. Unlike code coverage (collected automatically), functional coverage requires the verifier to declare the structure, instantiate a collector, and sample it at the right moment. Each step omitted breaks the measurement silently (0% or garbage).
  • The sample trigger decides what gets recorded. A covergroup records only the values present at the instant it is sampled — so when you sample (after the transaction's fields are valid and stable) determines whether the recording is meaningful. The trigger is the core design decision.
  • It is the integration point of coverage with the testbench. The covergroup connects to the monitor's stream of observed transactions (via an analysis-port subscriber), so coverage is collected from what the design actually did — the same observed transactions the scoreboard checks.
  • It accumulates the completeness number. The covergroup persists across the run, accumulating hits, and reports the percentage — the measure of "did we exercise enough?" that drives the coverage-closure loop (19.1).

The motivation, in one line: the coverage philosophy (measure completeness) needs a concrete instrument, and the covergroup is it — a declared, instantiated, sampled construct that watches the monitor's observed transactions and records which scenarios occurred into bins, accumulating the completeness percentage — so understanding how it is built and triggered (and the silent failures when a step is omitted) is essential to trusting the coverage number.

Mental Model

Hold the covergroup as a structured tally sheet — you make a mark each time you trigger it, and the blank categories at the end are the gaps:

A covergroup is a field researcher's structured tally sheet: it has categories (coverpoints) and sub-categories (bins), and each time you observe an event and trigger a tally (sample), it makes a mark in the matching category for the values you saw at that instant. It accumulates marks over the whole study, and at the end the filled-in categories show what was observed while the blank ones are the gaps — but it only ever records when you trigger it, on whatever values are present at that moment. Picture a field researcher studying traffic at an intersection, holding a structured tally sheet. The sheet has categories (coverpoints — e.g. "vehicle type") and sub-categories (bins — "car", "truck", "bike"). The researcher is the covergroup. Each time a vehicle passes (a transaction occurs), the researcher makes a tally mark (a sample) in the matching sub-category for what they saw at that instant. Three things define the instrument. First, the sheet must exist — a blank, instantiated sheet (new()); a researcher who never picked up a sheet (no instance) records nothing, no matter how much traffic passes. Second, the researcher only marks when they look up and tally — if they're distracted and never make a mark (no sample()), the sheet stays blank even though vehicles passed (0% coverage despite exercise). Third, the mark reflects the instant of tallying — if they glance at the wrong moment (sample before the vehicle is fully in view), they mis-categorize (record garbage). Over the whole study, the marks accumulate — the sheet builds up a record of what passed. At the end, the filled-in categories show what was observed, and the blank categories (no truck ever tallied) are the gaps — the scenarios never seen. The sheet (covergroup) is the instrument; the tally mark (sample) is the act of recording; the categories/sub-categories (coverpoints/bins) are the structure; and the when-to-tally (the trigger) must be at the right moment (when the observation is valid).

So the covergroup is a structured tally sheet: it must exist (instantiated — new()), it only records when triggered (sampled), it records the values present at the instant of triggering (so when matters), and it accumulates marks into a record whose blank categories are the gaps. Declaring the sheet's structure, instantiating a blank one, and triggering a tally at the right moment on the right observation are the three acts that make coverage real — and omitting any one leaves the sheet blank or wrong. The covergroup tallies what you trigger it on; build it, instantiate it, and trigger it at the right moment, or it records nothing or garbage.

Visual Explanation — the covergroup as a container of coverpoints

The defining picture is the containment: a covergroup holds coverpoints (and crosses), is instantiated as a collector, and is sampled to record.

A covergroup contains coverpoints and crosses, instantiated and sampled as a unitCovergroup (the container)declared, instantiated (new), and sampled as a unit — the measurable artifactdeclared, instantiated (new), and sampled as a unit — the measurable artifactCoverpoints (its contents)each measures one variable into bins — cp_op, cp_size, cp_resp (Module 19.3)each measures one variable into bins — cp_op, cp_size, cp_resp (Module 19.3)Crosses (its contents)combinations of coverpoints — op × size (Module 19.5)combinations of coverpoints — op × size (Module 19.5)Sample triggercg.sample(txn) records the current values into all coverpoints' bins at oncecg.sample(txn) records the current values into all coverpoints' bins at once
Figure 1 — a covergroup groups coverpoints and crosses, and is sampled to record. The covergroup is the container: it holds one or more coverpoints (each measuring a variable into bins) and crosses (combinations of coverpoints). It is instantiated as a live collector and sampled — each sample records the current values into the coverpoints' bins. The covergroup is the unit that is declared, instantiated, and sampled; the coverpoints and crosses are its contents (detailed in the next chapters). Sampling the covergroup records all its coverpoints at once.

The figure shows the covergroup as a container. The covergroup (the brand-colored top) is the unit that is declared, instantiated, and sampled — the measurable artifact. Its contents are coverpoints (the success-colored layer — each measures one variable into bins: cp_op, cp_size, cp_resp, detailed in Module 19.3) and crosses (combinations of coverpoints — op × size, Module 19.5). The sample trigger (the warning-colored layer — cg.sample(txn)) records the current values into all the coverpoints' bins at once. The crucial reading is the containment and granularity: the covergroup is the unit you declare/instantiate/sample, but the measurement happens in its contents (the coverpoints and crosses). One sample() call records all the coverpoints simultaneously — the covergroup is sampled as a whole, and each of its coverpoints captures its variable's current value into the matching bin. This is why the covergroup is the right granularity for the plan: a covergroup typically corresponds to one interface or transaction type, holding all the coverpoints (the features to measure) and crosses (the combinations) for that transaction, sampled together when a transaction is observed. The diagram is the anatomy: the covergroup (container, declared/instantiated/sampled) holds coverpoints and crosses (contents, the actual measurements) and is triggered by sample (recording all contents at once). The next chapters detail the contents (coverpoints 19.3, bins 19.4, cross 19.5); this chapter is the container and its lifecycle. The covergroup is the unit you build and trigger; its coverpoints are what it measures.

RTL / Simulation Perspective — declare, instantiate, sample

In code, the covergroup's three-step lifecycle is explicit. The example shows the idiomatic UVM form: a covergroup with a sample function, embedded in a coverage collector, instantiated in the constructor, and sampled per transaction.

the covergroup lifecycle: declare (structure), instantiate (new), sample (trigger)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
class coverage_collector extends uvm_subscriber #(bus_txn);
  `uvm_component_utils(coverage_collector)
 
  // (1) DECLARE — the covergroup's STRUCTURE, with a sample FUNCTION taking the transaction
  covergroup txn_cg with function sample(bus_txn t);
    cp_op:   coverpoint t.op   { bins ops[]  = {READ, WRITE, RMW}; }       // coverpoint (19.3)
    cp_size: coverpoint t.size { bins small = {[1:4]}; bins large = {[5:64]}; }
    x_op_size: cross cp_op, cp_size;                                        // cross (19.5)
  endgroup
 
  function new(string name, uvm_component parent);
    super.new(name, parent);
    txn_cg = new();          // (2) INSTANTIATE — covergroups are NOT auto-constructed; MUST new() it
  endfunction                //     (forgetting this → null handle → records NOTHING — DebugLab)
 
  // (3) SAMPLE — the analysis-port write() triggers the covergroup on EACH observed transaction
  function void write(bus_txn t);
    txn_cg.sample(t);        // record t's CURRENT field values into the coverpoints' bins
  endfunction                //     (only records what it is sampled on — wire this, or coverage = 0%)
endclass
// the covergroup ACCUMULATES across the run and reports a percentage = the completeness measure (19.1)

The code shows the three-step lifecycle in its idiomatic UVM form. (1) Declare: covergroup txn_cg with function sample(bus_txn t); ... endgroup defines the structure — the coverpoints (cp_op, cp_size — Module 19.3) and cross (x_op_size — 19.5) — and the sample function sample(bus_txn t) that takes the transaction so the coverpoints can reference t's fields. (2) Instantiate: txn_cg = new() in the constructor — covergroups are not auto-constructed (unlike uvm_component_utils-built components), so you must new() the covergroup explicitly, or its handle is null and it records nothing (the DebugLab). (3) Sample: the write() method (from uvm_subscriber, called by the monitor's analysis port on each observed transaction) calls txn_cg.sample(t)recording t's current field values into the coverpoints' bins. The shape to carry: the covergroup is declared (structure), instantiated (live collector — don't forget new()), and sampled (triggered per transaction via the subscriber's write()). The sample function (with function sample(...)) is the idiomatic UVM choice — it lets the covergroup sample a transaction object (the same object the scoreboard checks), rather than signals via an event. The covergroup then accumulates across the run and reports the percentage — the completeness measure (19.1). The embedding in a uvm_subscriber (#(bus_txn), with write()) is the standard wiring: the monitor broadcasts observed transactions on its analysis port, and this subscriber receives them and samples the covergroup — coverage collected from what the design actually did. Declare the structure, new() the collector, sample it per transaction from the analysis-port write().

Verification Perspective — where the covergroup lives in the testbench

The covergroup doesn't sample itself — it's triggered by the monitor's stream of observed transactions, through an analysis-port subscriber. Seeing the wiring shows where coverage plugs into the testbench.

Monitor analysis port feeds both a coverage subscriber (covergroup sample) and the scoreboardobserved txnwrite(t)write(t)cg.sample(t)accumulates → %Monitorobserves the DUTAnalysis portbroadcasts txns (19.x)Coverage subscriberwrite() per txnScoreboardchecks (Module 18)Covergroupsample(t)Coverage %completeness12
Figure 2 — where the covergroup lives: a coverage subscriber on the monitor's analysis port. The monitor observes the DUT and broadcasts each transaction on its analysis port. A coverage collector (a uvm_subscriber) connects to that port; its write() is called on every observed transaction and calls cg.sample(t). The same analysis port also feeds the scoreboard (Module 18) — so coverage and checking both observe the identical transaction stream. Coverage is collected from what the design actually did, not from the stimulus.

The figure shows where the covergroup lives. The monitor observes the DUT and broadcasts each transaction on its analysis port. A coverage subscriber (a uvm_subscriber) connects to that port; its write() is called on every observed transaction and calls cg.sample(t) on the covergroup, which accumulates into the coverage %. The crucial reading is the shared analysis port: the same port also feeds the scoreboard (Module 18) — so coverage and checking both observe the identical transaction stream. This is the structural expression of 19.1's orthogonality: the monitor produces one stream of observed transactions, and two subscribers consume it for two purposes — the scoreboard checks correctness, the coverage collector measures completeness. The coverage is collected from what the design actually did (the observed transactions), not from the stimulus (what we intended to drive) — which matters, because coverage on the stimulus would measure what we asked for, while coverage on the monitor measures what the DUT actually saw and did (including any reordering or transformation). The brand-colored analysis port and covergroup, with the success-colored subscriber between, form the coverage path; the parallel path to the scoreboard shows the orthogonal check. The verification insight is that the covergroup is not a standalone thing — it's wired into the testbench at the monitor's analysis port, exactly like the scoreboard, reusing the analysis/TLM infrastructure (Module 17). So building coverage is building a subscriber: connect it to the monitor's port, and sample the covergroup in its write(). The diagram is the coverage wiring: monitor → analysis port → coverage subscriber → covergroup.sample() → percentage, alongside the scoreboard on the same port — one observed stream, checked and measured. Coverage plugs in where the scoreboard does: the monitor's analysis port.

Runtime / Execution Flow — the covergroup lifecycle over a run

At run time, the covergroup goes through construction once then sample-and-accumulate many times, ending in a reported percentage. The flow shows it.

Covergroup lifecycle: construct once, sample per transaction, report percentage at the endconstruct (new the covergroup, once) → sample per observed transaction (accumulate hits) → report the accumulated percentage at the endconstruct (new the covergroup, once) → sample per observed transaction (accumulate hits) → report the accumulated percentage at the end1Construct (once)the coverage collector is created and the covergroup isinstantiated with new() — a blank tally sheet.2Sample per transactioneach observed transaction arrives at write(), which callscg.sample(t), recording the current values into the bins.3Accumulate hitseach sample adds to the running record — which bins have been hitso far across the run.4Report the percentageat the end, the covergroup reports the accumulated coverage % — thecompleteness measure, merged across runs.
Figure 3 — the covergroup lifecycle over a run. At build/construction, the coverage collector is created and the covergroup is instantiated (new). During the run, every observed transaction arrives at the subscriber's write(), which samples the covergroup, recording the current values and accumulating hits. At the end, the covergroup reports the accumulated coverage percentage. Construction happens once; sampling happens per transaction; the percentage is the run's completeness measure — merged across runs in the coverage database.

The flow shows the lifecycle over a run. Construct (step 1, once): the coverage collector is created and the covergroup is instantiated with new() — a blank tally sheet. Sample per transaction (step 2): each observed transaction arrives at the subscriber's write(), which calls cg.sample(t), recording the current values into the bins. Accumulate (step 3): each sample adds to the running recordwhich bins have been hit so far across the run. Report (step 4): at the end, the covergroup reports the accumulated coverage percentage — the completeness measure, merged across runs in the coverage database. The runtime insight is the cardinality of each step: construction happens once (the covergroup is instantiated at build/construction time — forget it and every sample is on a null handle), but sampling happens per transaction (many times, accumulating). The covergroup persists across the whole run — it's not re-created per transaction; it's a single accumulating collector. The percentage is cumulative: it only goes up (or stays flat) as the run exercises more scenarios. And across runs (a regression of many random seeds), the per-run coverage is merged in the coverage database — so total coverage is the union of all runs' hits (this is why constrained-random with many seeds closes coverage: each seed hits a different subset, and the merge accumulates them). The flow is the covergroup's life: constructed once (blank sheet), sampled per transaction (tally marks), accumulating (the record builds), reported at the end (the percentage), merged across runs (the union). The runtime shapeconstruct once, sample many, accumulate, report — is why the instantiation (once) and the sample wiring (per transaction) are both essential and both have distinct failure modes (no instance → null; no sample → 0%). Build the sheet once; tally on every transaction; the percentage accumulates to the completeness measure.

Waveform Perspective — sampling at the right moment

The sample trigger is a timed event: the covergroup records the values present at the instant of sampling, so it must fire when the transaction's fields are valid. The waveform shows correct sampling, and a mistimed sample.

The covergroup samples when the transaction is valid; a sample before validity records stale values

12 cycles
The covergroup samples when the transaction is valid; a sample before validity records stale valuestxn_done → data=C1 valid → cg_sample fires → records C1 (good_sample)txn_done → data=C1 val…txn_done → data=D2 valid → cg_sample fires → records D2txn_done → data=D2 val…bad_sample (early): data still ?? → would record a stale/garbage valuebad_sample (early): da…clkdata--??C1C1--??D2D2--------txn_donecg_samplegood_samplebad_samplet0t1t2t3t4t5t6t7t8t9t10t11
Figure 4 — sampling at the right moment. A transaction completes when txn_done pulses, at which point its fields (data) are valid and stable. The covergroup's sample (cg_sample) fires aligned with txn_done — recording the valid data into the bins (good_sample). If instead the sample fired early, before the fields settled (bad_sample at cycle 2, while data is still stale), it would record the wrong value into the wrong bin — coverage that looks healthy but measures garbage. The sample trigger must align with the moment the transaction's values are valid.

The waveform shows sampling at the right moment. A transaction completes when txn_done pulses, at which point its fields (data) are valid and stable (C1, then D2). The covergroup's sample (cg_sample) fires aligned with txn_donerecording the valid data into the bins (good_sample). The crucial reading is the alignment: the sample must fire when the transaction's values are validhere, at txn_done, when data has settled. If instead the sample fired early (bad_sample at cycle 1, while data is still ??stale/uninitialized), it would record the wrong value into the wrong bincoverage that looks healthy (a bin got hit) but measures garbage (the wrong bin, from a stale value). This is the covergroup's timing discipline: the sample trigger must align with the moment the transaction's fields are valid — in UVM with a sample function, this is naturally handled because the monitor only broadcasts a completed, assembled transaction (Module 13.2), so write() is called (and the covergroup sampled) after the transaction's fields are valid by construction. The waveform makes visible why the transaction-level sampling (sample the assembled transaction, not raw signals mid-flight) is correct: the transaction object holds the valid, final field values, so sampling it records the right values. The picture to carry is that coverage is only as good as the sample timing: a well-timed sample (at transaction completion, on the assembled object) records meaningful coverage; a mistimed one (on stale signals) records garbage that inflates the percentage falsely. Reading the waveform this way — does the sample fire when the values are valid? — is verifying the sample trigger. The sample aligned with txn_done recording valid data is the signature of correct coverage sampling — which the transaction-level UVM pattern (sample the monitored transaction) gives you by construction. Sample the assembled transaction when it is complete, not raw signals mid-flight.

DebugLab — the covergroup that was never sampled

0% coverage on a scenario you know ran — a covergroup that was never triggered

Symptom

A team's coverage report showed a covergroup at 0%no bins hit, every coverpoint empty — even though they knew the scenarios had run: the regression had driven thousands of READ, WRITE, and RMW transactions, the scoreboard had checked them all (and passed), and the waveforms clearly showed the operations occurring. Yet the covergroup reported nothing exercised. The team, trusting the 0%, started writing more tests to "hit" the operations — scenarios that were already running — and the coverage stayed at 0% no matter how many transactions they drove.

Root cause

The covergroup was declared and instantiated, but its sample was never triggered — the coverage subscriber was never connected to the monitor's analysis port (or write() never called sample()), so no transaction ever reached the covergroup. A covergroup records only what it is sampled on, and this one was sampled on nothing:

why a covergroup that runs reports 0% — the sample was never wired
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
✗ DECLARED and INSTANTIATED, but never SAMPLED (subscriber not connected):
  // covergroup txn_cg ...; endgroup     // declared
  // txn_cg = new();                     // instantiated — a blank tally sheet
  // ...but the coverage_collector's analysis export was NEVER connected to the monitor's port
  //    → write() is NEVER called → txn_cg.sample(t) NEVER runs → 0% despite thousands of txns
  // the scenarios RAN; the covergroup just never WATCHED them → a FALSE gap
 
✓ WIRE the sample trigger — connect the subscriber to the monitor's analysis port:
  // in the env's connect_phase:
  monitor.ap.connect(cov_collector.analysis_export);   // now write() fires per observed txn
  // write() calls txn_cg.sample(t) → coverage accumulates → the real picture appears
  // ALWAYS verify a covergroup is actually being sampled before trusting a low coverage number

This is the never-sampled covergroup bug — the quintessential covergroup wiring mistake. The covergroup was declared (structure defined) and instantiated (new() — a blank tally sheet), so it existed — but its sample was never triggered, because the coverage collector's analysis export was never connected to the monitor's analysis port (Module 17.2 / Figure 2). With no connection, the subscriber's write() is never called, so txn_cg.sample(t) never runs — the covergroup watches nothing and reports 0%, no matter how many transactions run. The tell0% on a scenario you know ran (confirmed by the scoreboard and waveforms) — is the signature of a sampling failure, not a real gap: the scenarios ran, but the covergroup never watched them. The false 0% is dangerous because it misdirects: the team wrote more tests to "hit" operations that were already running, and coverage stayed at 0% (more unwatched transactions don't help). The fix is to wire the sample trigger: connect the coverage collector's analysis export to the monitor's analysis port in the env's connect_phase (monitor.ap.connect(cov_collector.analysis_export)), so write() fires per observed transaction and samples the covergroup — and the real coverage appears. The general lesson, and the chapter's thesis: a covergroup records only what it is sampled on — it must be declared, instantiated (new()), and triggered (its sample wired to the right event), and omitting the trigger yields 0% (a false gap) despite the scenarios running; so always verify the covergroup is actually being sampledconnect the subscriber, confirm write() calls sample()before trusting a low coverage number, because 0% may mean "never watched," not "never exercised." A covergroup that isn't sampled reports a gap that isn't there; wire the trigger before you chase the gap.

Diagnosis

The tell is 0% (or implausibly low) coverage on a scenario you know ran. Diagnose a never-sampled covergroup:

  1. Confirm the scenario actually ran. Check the scoreboard saw the transactions and the waveforms show them — if so, 0% is a sampling failure, not a real gap.
  2. Check the covergroup was instantiated. A null handle (no new()) records nothing; verify the new() in the constructor.
  3. Check the subscriber is connected. Verify the coverage collector's analysis export is connected to the monitor's analysis port in connect_phase.
  4. Check write() calls sample(). Confirm the subscriber's write() actually calls cg.sample(t) on the received transaction.
Prevention

Verify the covergroup is sampled before trusting the number:

  1. Always new() the covergroup in the constructor. Covergroups aren't auto-constructed; a missing new() is a null handle that records nothing.
  2. Connect the coverage subscriber to the monitor's analysis port. Wire it in connect_phase, like the scoreboard, so write() fires per transaction.
  3. Sanity-check coverage moves off zero early. A covergroup that stays at 0% after transactions run is a sampling bug, not a stimulus gap.
  4. Sample the assembled transaction, not raw signals. Use a sample function on the monitored transaction so the values are valid by construction.

The one-sentence lesson: a covergroup records only what it is sampled on, so it must be declared, instantiated with new(), and triggered by a sample wired to the right event (the monitor's analysis port) — omit the trigger and it reports 0% despite the scenarios running, so always verify the covergroup is actually being sampled before trusting a low coverage number, because 0% may mean "never watched," not "never exercised."

Common Mistakes

  • Forgetting to new() the covergroup. Covergroups aren't auto-constructed; a missing new() is a null handle that records nothing — instantiate it in the constructor.
  • Never wiring the sample trigger. A declared, instantiated covergroup that's never sampled reads 0% though the scenarios ran; connect the subscriber and call sample().
  • Sampling at the wrong moment. Sampling stale signals before the transaction's fields are valid records garbage; sample the assembled transaction at completion.
  • Sampling the stimulus instead of the monitor. Coverage should measure what the DUT actually did (the observed transactions), not what you intended to drive.
  • Trusting a low coverage number without checking sampling. 0% on a scenario you know ran is a sampling bug, not a real gap; verify the wiring first.
  • Putting unrelated coverpoints in one covergroup. A covergroup should group the coverpoints for one transaction/interface, sampled together on that transaction.

Senior Design Review Notes

Interview Insights

A covergroup is the SystemVerilog construct that groups a set of coverpoints and crosses and is sampled to record the current values into their bins — it's the concrete instrument that implements functional coverage, turning the coverage plan into a measurable artifact. The three steps are declare, instantiate, and sample. First, declare: you write covergroup name with function sample, then the coverpoints and crosses, then endgroup. This defines the structure — which variables are measured, into which bins, and which combinations are crossed. The with function sample part is the idiomatic UVM form: it lets the covergroup take a transaction as an argument so the coverpoints can reference the transaction's fields. Second, instantiate: you call name equals new in the constructor. This is crucial because covergroups are not automatically constructed the way uvm_component_utils components are — you must explicitly new the covergroup, or its handle is null and it records nothing. Third, sample: you call cg dot sample, passing the transaction, at the right moment. In UVM this is typically done in the write method of a coverage subscriber connected to the monitor's analysis port, so the covergroup is sampled on every observed transaction. Each sample records the transaction's current field values into the coverpoints' bins. The covergroup then accumulates these hits across the whole run and reports a percentage at the end — the completeness measure. The mental model is a structured tally sheet: it must exist (instantiated), it only records when you trigger it (sampled), it records the values present at the instant of triggering, and it accumulates marks whose blank categories at the end are the coverage gaps. The key point is that none of this is automatic — unlike code coverage, you build the structure, instantiate the collector, and wire the trigger, and omitting any step breaks the measurement silently.

A covergroup typically lives inside a coverage collector — a class that extends uvm_subscriber — which connects to the monitor's analysis port, and it's triggered by the subscriber's write method on every observed transaction. The flow is: the monitor observes the DUT and assembles each transaction, then broadcasts it on its analysis port. A coverage collector subscribes to that port; because it extends uvm_subscriber, it has a write method that the analysis port calls for every transaction. Inside write, you call cg dot sample, passing the received transaction, which records its field values into the covergroup's coverpoints. So the trigger is the analysis port delivering a transaction to write, which samples the covergroup. The wiring is done in the environment's connect_phase: you connect the monitor's analysis port to the coverage collector's analysis export, exactly like you connect the scoreboard. In fact, the same analysis port usually feeds both the coverage collector and the scoreboard — one observed transaction stream, consumed by two subscribers for two orthogonal purposes: the scoreboard checks correctness, the coverage collector measures completeness. This is important for two reasons. First, it means coverage is collected from what the design actually did — the monitored transactions — not from the stimulus, so it reflects the DUT's real behavior including any reordering or transformation. Second, it reuses the analysis and TLM infrastructure, so building coverage is just building another subscriber. The covergroup must also be instantiated with new in the coverage collector's constructor, because it isn't auto-constructed. A common bug is forgetting to connect the subscriber, so write is never called and the covergroup reports 0% even though the scenarios ran — a false gap. So the covergroup lives in a subscriber on the monitor's analysis port, is instantiated in the constructor, and is sampled per transaction in write.

The sample function is the with function sample clause in a covergroup declaration that lets the covergroup take arguments — typically a transaction object — so its coverpoints can reference that object's fields, and it's the idiomatic way to do transaction-level coverage in UVM. You write covergroup txn_cg with function sample, then the transaction type and name, like bus_txn t, then the coverpoints reference t dot field. When you call txn_cg dot sample, passing a transaction, the covergroup records that transaction's field values into the bins. The reason to use it is that it decouples the covergroup from any particular variable in scope and lets you sample a passed-in object explicitly. Without it, a covergroup samples variables that are in its enclosing scope, and it's triggered either by an explicit sample call or by an event in the declaration like at posedge clk. The event form samples signals directly, which means you sample raw signals at a clock edge — fine for signal-level coverage, but for transaction-level coverage you want to sample the assembled transaction object after the monitor has reconstructed it. The sample function gives you exactly that: you pass the completed transaction, and the coverpoints measure its fields. This has two big advantages. First, timing correctness: the monitor only broadcasts a transaction after its fields are valid and assembled, so sampling the transaction object records valid values by construction — you avoid the trap of sampling stale signals mid-transaction. Second, it samples the same transaction object the scoreboard checks, so coverage and checking are on the identical observed stream. So the sample function is what makes transaction-level functional coverage clean: the covergroup samples a passed transaction, its coverpoints measure the transaction's fields, and you call it from the coverage subscriber's write method on each observed transaction. It's the standard pattern for UVM functional coverage.

Because the covergroup was never sampled — it records only what it's sampled on, and if the sample trigger isn't wired, no transaction ever reaches it, so it reports 0% no matter how many scenarios run. There are two common causes. One, the covergroup was never instantiated: covergroups aren't auto-constructed, so if you forgot to call new in the constructor, the handle is null and sampling does nothing. Two, and more common, the covergroup was declared and instantiated but the sample was never triggered — the coverage subscriber was never connected to the monitor's analysis port, or write never actually calls sample. With no connection, write is never called, so cg dot sample never runs, and the covergroup watches nothing. The tell is distinctive: 0% coverage on a scenario you know ran — confirmed because the scoreboard saw and checked those transactions and the waveforms show them occurring. That combination — scenarios definitely ran, coverage definitely zero — means a sampling failure, not a real gap. It's dangerous because it misdirects: the team trusts the 0% and writes more tests to hit operations that are already running, and coverage stays at zero because more unwatched transactions don't help. The fix is to wire the sample trigger: connect the coverage collector's analysis export to the monitor's analysis port in connect_phase, so write fires per transaction and samples the covergroup, and verify write actually calls sample. The discipline is to always verify a covergroup is actually being sampled before trusting a low coverage number, because 0% may mean never watched, not never exercised. A good sanity check is that coverage should move off zero early once transactions start running; if it stays pinned at zero, suspect the wiring, not the stimulus. So a covergroup must be declared, instantiated, and triggered, and the trigger is the part most often forgotten.

You sample the monitored transaction because it holds valid, assembled field values, whereas raw signals can be stale or mid-transition, and timing matters because a covergroup records whatever values are present at the instant you sample — sample at the wrong moment and you record garbage. A covergroup is a snapshot: when you call sample, it captures the current values of the variables its coverpoints reference and records them into bins. So the values must be valid at that instant. If you sample raw DUT signals at, say, a clock edge before the transaction's fields have settled, you capture stale or uninitialized values and record them into the wrong bins — coverage that looks healthy because bins got hit, but measures garbage. The monitored transaction avoids this. The monitor watches the bus, waits for a transaction to complete, and assembles a transaction object with the final, valid field values. By the time the monitor broadcasts it on the analysis port and your subscriber's write samples the covergroup, the fields are valid by construction. So sampling the transaction object — via a sample function that takes the transaction — records correct values, because the monitor has already done the work of waiting for validity and assembling the fields. This is why the transaction-level pattern is the standard: sample the assembled transaction, not signals mid-flight. The timing discipline is automatically handled because the monitor only emits completed transactions. If you instead used an event-triggered covergroup sampling signals at every clock, you'd have to carefully ensure the sample event aligns with field validity, which is error-prone. The waveform view makes it concrete: the sample should fire when the transaction-done condition holds and the data is stable, recording the valid value; a sample one cycle early, while data is still settling, records the wrong value. So sampling the monitored transaction at completion gives you correct values for free, while sampling raw signals requires you to get the timing exactly right, and getting it wrong silently corrupts the coverage.

Exercises

  1. Write the lifecycle. Sketch a covergroup with a sample function, instantiated in a constructor and sampled in a subscriber's write() — labeling declare, instantiate, sample.
  2. Wire it in. Describe the connect_phase wiring that connects a coverage collector to the monitor's analysis port.
  3. Diagnose the 0%. A covergroup reads 0% though the scoreboard saw the transactions. List the checks that find the sampling failure.
  4. Explain the timing. Explain why sampling the monitored transaction records valid values while sampling raw signals might not.

Summary

  • A covergroup groups a set of coverpoints (and crosses) and is sampled to record the current values into their bins — the concrete instrument that turns the coverage plan into a measurable artifact.
  • Its three-step lifecycle: declare (covergroup ... endgroup — the structure), instantiate (cg = new() — a live collector; covergroups are not auto-constructed, so a missing new() records nothing), and sample (cg.sample(txn) — records the current values; it records only what it is sampled on).
  • The idiomatic UVM form uses a sample function (with function sample(bus_txn t)) so the covergroup samples a transaction object and its coverpoints measure t's fields — valid by construction, the same object the scoreboard checks.
  • It lives in a coverage collector (a uvm_subscriber) on the monitor's analysis portwrite() samples the covergroup per observed transaction (alongside the scoreboard on the same port), so coverage measures what the design actually did; it accumulates across the run and reports the percentage.
  • The durable rule of thumb: a covergroup is a tally sheet that records only what it is sampled on — declare its structure, new() it in the constructor, and wire its sample to the monitor's analysis port so it triggers on every observed transaction at the moment the fields are valid; if it reads 0% on a scenario you know ran, suspect the sampling wiring (never watched), not the stimulus (never exercised) — verify the covergroup is actually being sampled before trusting its number.

Next — Coverpoints: the covergroup's contents — the coverpoint: how it measures a single variable, the values it samples, how those values map to bins, and how a set of coverpoints expresses the features of a transaction the plan says to exercise.