Skip to content

SystemVerilog · Module 11

Sampling Events

The mechanism that determines when coverage data lands in the bins. Event-driven vs manual sampling, the @(event) syntax variants, conditional sampling with iff, argument-bearing samples via `with sample()`, the Postponed-region timing model, and why the sampling event is part of every coverage contract.

Module 11 · Page 11.9

Why the Sampling Event Matters as Much as the Bins

Modules 11.2 through 11.8 built the structural and option vocabulary — covergroups, coverpoints, bins, crosses, options. None of that machinery measures anything until a sample event fires. A sample event is the simulator's "freeze the current value of every coverpoint expression and increment the matching bins" trigger.

There are two ways to fire one: the covergroup can declare an event (covergroup cg @(posedge clk);) and the simulator fires automatically, or the testbench can call cg.sample() explicitly. Both are legal; they suit different verification layers. The choice you make is not just a syntactic preference — it becomes part of every bin's contract. A (RESET => IDLE) transition bin measured at every clock edge says something different from the same bin measured only at committed transactions. The bin definition is identical; the semantic is not.

Event-Driven Sampling — The @(event) Form

The covergroup declares an event in its header; the simulator samples every time the event fires. The event can be a clock edge, a named event, a wait condition, or @(*) (sample on any change in any sampled expression).

Syntax variants

SystemVerilog — event-driven sampling forms
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ── Clock-edge sampling — the most common form ────────────────────
covergroup cg_clk @(posedge clk);
    cp_state: coverpoint state;
endgroup
 
// ── Conditional clock-edge — only when a qualifier is high ────────
covergroup cg_iff @(posedge clk iff vif.tvalid);
    cp_state: coverpoint state;
endgroup
 
// ── Named event — fires when the event is triggered ───────────────
event txn_committed;
covergroup cg_event @(txn_committed);
    cp_state: coverpoint state;
endgroup
// ... elsewhere in the testbench ...
//   -> txn_committed;   // explicit event trigger fires the sample
 
// ── @(*) — sample on any change of any sampled expression ─────────
covergroup cg_any @(state, opcode);
    cp_state:  coverpoint state;
    cp_opcode: coverpoint opcode;
endgroup
// Samples whenever `state` or `opcode` changes value.
 
// ── Edge-and-condition combinations ───────────────────────────────
covergroup cg_combined @(posedge clk iff (rst_n && state != IDLE));
    cp_state: coverpoint state;
endgroup

The reading rule: the event expression after @() is identical in syntax to the expression that would go in an always @(...) block — clock edges, named events, signal lists, and iff qualifiers all compose the same way. SystemVerilog reuses the existing event-control vocabulary for coverage sampling.

What the simulator does when an event fires

When the sampling event fires, the simulator:

  1. Pauses scheduled NBA updates at the current time step (sampling runs in the Postponed region — after every signal has settled).
  2. Evaluates every coverpoint expression against the settled values for the current time step.
  3. Updates the bin counters for every matching bin in every coverpoint and cross.
  4. Fires any illegal_bins violation if a hit lands in an illegal bin.

The Postponed-region semantics is critical: coverage samples the final committed value at the sample instant, not transient mid-cycle values. RTL that briefly produces an illegal encoding for one delta-cycle before settling will not trigger an illegal-bin hit — the sample only sees the settled value. For sub-cycle contract enforcement, SVA assertions (Module 12) are the right complement.

Manual Sampling — cg.sample()

A covergroup declared without a header event (covergroup cg; rather than covergroup cg @(event);) samples only when the testbench calls cg.sample() explicitly. The form puts the testbench in control of when the sample happens.

SystemVerilog — manual sampling forms
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
class apb_txn;
    rand bit [31:0] paddr;
    rand bit         pwrite;
 
    covergroup cg_apb;           // ← no @event; manual only
        cp_addr: coverpoint paddr;
        cp_dir:  coverpoint pwrite;
    endgroup
 
    function new();
        cg_apb = new();
    endfunction
 
    // The canonical pattern — sample after each randomization
    function void post_randomize();
        cg_apb.sample();
    endfunction
 
    // The monitor-driven pattern — sample at the moment the monitor
    // confirms the transaction was committed by the slave
    function void sample_at_commit();
        cg_apb.sample();
    endfunction
endclass

When manual sampling is the right choice. Almost every transaction-level covergroup: the right moment to sample is "the transaction was fully committed," which is a software event the monitor controls, not a hardware event the clock controls. Manual sampling gives the monitor the authority to decide when the sample fires; event-driven sampling would either over-sample (every clock edge during the transaction's drive window) or under-sample (only on the clock edge after commit, which may not be the right beat).

The discipline rule. Manual sampling is more flexible and more fragile — the testbench is responsible for sample timing, and a bug in the sample-call placement breaks coverage silently. Code review should treat every cg.sample() call as semantically meaningful — its placement is part of the coverage contract, the same way bin definitions are.

with function sample() — Sampling with Arguments

A covergroup can declare a custom sample() function that takes arguments, replacing the simulator's default no-argument sample. The arguments become the values the coverpoints see — useful when the sampled values are not directly available in the covergroup's scope but are passed at sample time.

SystemVerilog — covergroup with sample arguments
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
covergroup cg_apb with function sample(bit [31:0] paddr_arg,
                                          bit         pwrite_arg,
                                          bit [31:0] pwdata_arg);
    cp_addr  : coverpoint paddr_arg;
    cp_dir   : coverpoint pwrite_arg;
    cp_data  : coverpoint pwdata_arg;
    cr_addr_dir: cross cp_addr, cp_dir;
endgroup
 
// Usage from the monitor:
cg_apb cg = new();
// ... when the monitor sees a committed transaction:
cg.sample(.paddr_arg(observed_paddr),
          .pwrite_arg(observed_pwrite),
          .pwdata_arg(observed_pwdata));

Why this form exists. The coverpoint expressions inside the covergroup body now reference the function arguments, not signals or class fields. The monitor passes whatever values it wants — observed bus values, scoreboard-computed expected values, predictor outputs — without the covergroup needing access to those signals from its lexical scope. This is the canonical pattern for monitor-driven coverage where the sampled values come from a TLM analysis port or a UVM transaction object.

The variant with function sample(...) works alongside both event-driven and manual sampling: an event-driven covergroup with with function sample declares the argument list, and the simulator passes default values (0 or 'x) at event-firing time. In practice, with function sample is almost always paired with manual sampling — the testbench is calling the function explicitly anyway, and the arguments fit naturally into the call.

The Sampling Event as Part of the Bin Contract

Every transition bin (Module 11.6) and every cross bin (Module 11.7) carries an implicit dependency on the sampling event. The same bin definition produces different reports against different sampling events.

SystemVerilog — same bin, two sampling events, two semantics
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ── Covergroup A — samples every clock edge ──────────────────────
covergroup cg_clk @(posedge clk);
    cp_state: coverpoint state {
        bins reset_to_idle = (RESET => IDLE);
    }
endgroup
 
// ── Covergroup B — samples only on committed transactions ───────
event txn_committed;
covergroup cg_txn @(txn_committed);
    cp_state: coverpoint state {
        bins reset_to_idle = (RESET => IDLE);
    }
endgroup
 
// RTL timeline:
//   cycle 1: state = RESET     (no txn)
//   cycle 2: state = RESET     (no txn)
//   cycle 3: state = IDLE      (no txn)
//   cycle 4: state = IDLE      (-> txn_committed)
//   cycle 5: state = ACTIVE    (no txn)
//   cycle 6: state = ACTIVE    (-> txn_committed)
 
// Covergroup A samples 6 times: RESET, RESET, IDLE, IDLE, ACTIVE, ACTIVE
//   → reset_to_idle bin fires once (sample 2 → sample 3)
//
// Covergroup B samples 2 times: IDLE (at cycle 4), ACTIVE (at cycle 6)
//   → reset_to_idle bin NEVER fires (RESET never appeared in the sample sequence)
//
// Same bin definition. Different sampling event. Different report.

The contract rule that follows: the sampling event is not metadata; it is part of every transition and cross bin's specification. A covergroup change that swaps the sampling event silently changes the meaning of every transition and cross bin in the model. Code review should treat any covergroup ... @(...) line change with the same scrutiny it gives a structural bin edit.

For event-driven covergroups, document the sampling event in option.comment:

SystemVerilog — documenting the sampling-event contract
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
covergroup cg_apb @(posedge pclk iff vif.pready);
    option.comment = "Samples on every PCLK edge where PREADY is high — "
                       + "i.e., once per committed APB transfer "
                       + "(vplan §5.1)";
    cp_state: coverpoint state;
endgroup

The reader who later opens the file sees the sampling semantic immediately; the auditor who reads the coverage report can verify the bin definitions are evaluated at the spec-defined moment.

A Complete Working Example — Three Sampling Strategies in One Environment

The pattern below shows three coexisting covergroups, each measuring a different layer with a different sampling event.

SystemVerilog — three sampling strategies for one DUT
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
class apb_env;
    apb_vif vif;
    apb_txn observed_txn;
    mailbox #(apb_txn) mon2sb;
 
    // ── Layer 1 — Interface coverage (every clock edge) ───────────
    covergroup cg_iface @(posedge vif.pclk);
        option.comment = "Per-cycle interface coverage — captures every "
                           + "pin-level state including idle cycles";
        cp_psel:      coverpoint vif.psel;
        cp_penable:   coverpoint vif.penable;
        cp_pready:    coverpoint vif.pready;
        cr_handshake: cross cp_psel, cp_penable, cp_pready;
    endgroup
 
    // ── Layer 2 — Bus coverage (only on completed transfers) ──────
    covergroup cg_bus @(posedge vif.pclk iff (vif.psel && vif.penable && vif.pready));
        option.comment = "Per-transfer bus coverage — samples only on "
                           + "successful APB completion (PSEL && PENABLE && PREADY)";
        cp_paddr:  coverpoint vif.paddr;
        cp_pwrite: coverpoint vif.pwrite;
        cr_aw:     cross cp_paddr, cp_pwrite;
    endgroup
 
    // ── Layer 3 — Transaction coverage (monitor-driven, manual) ──
    covergroup cg_txn with function sample(apb_txn t);
        option.comment = "Per-transaction coverage — sampled by the monitor "
                           + "at commit; sees the reconstructed transaction object";
        cp_paddr_class: coverpoint t.paddr {
            bins csr = {[32'h0000_0000 : 32'h0000_00FF]};
            bins ram = {[32'h1000_0000 : 32'h1FFF_FFFF]};
        }
        cp_dir: coverpoint t.pwrite;
        cr_class_dir: cross cp_paddr_class, cp_dir;
    endgroup
 
    function new();
        cg_iface = new();
        cg_bus   = new();
        cg_txn   = new();
    endfunction
 
    // Monitor body — drives manual sample at the abstraction layer
    task monitor();
        forever begin
            mon2sb.get(observed_txn);
            cg_txn.sample(observed_txn);    // ← manual sample
        end
    endtask
endclass

What this code does. Three covergroups in the same environment, each measuring at a different layer:

  • cg_iface samples every clock edge — captures interface-pin coverage including idle cycles, used for "did we exercise every (PSEL, PENABLE, PREADY) state combination" coverage at the signal level.
  • cg_bus samples only on the edge where PSEL, PENABLE, and PREADY are all high — the moment of a successful APB transfer. This is bus-layer coverage; the report counts one sample per completed transfer.
  • cg_txn samples manually, called by the monitor with the reconstructed transaction object as argument. This is transaction-layer coverage; the sample timing is fully decoupled from the clock.

Expected output (urg-style, partial).

Three layers of coverage from one regression
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
cg_iface  (per-cycle)                          coverage 100.0%
  cr_handshake                                  100.0%  ( 8/8 bins; some at  3 hits, some at 8000 )
  ... (all interface-pin combinations exercised)
 
cg_bus  (per-transfer)                         coverage 100.0%
  cp_paddr                                      100.0%  (all regions hit)
  cp_pwrite                                     100.0%  (read + write)
  cr_aw                                         100.0%  (region × direction)
 
cg_txn  (per-transaction)                      coverage  75.0%
  cp_paddr_class                                100.0%  (csr + ram)
  cp_dir                                        100.0%  (read + write)
  cr_class_dir                                   50.0%  (2/4 — csr × write empty,
                                                          ram × read empty)

What to read out of this. The interface and bus layers are at 100% — the testbench drove every pin-level state and every bus transfer category. The transaction layer shows real gaps: csr × write and ram × read are at 0%. The three-layer model surfaces the gap that any one layer alone would have missed — interface coverage at 100% does not imply transaction coverage at 100%, and vice versa.

Common Pitfalls — Where Sampling Goes Wrong

Pitfall 1 — Sampling on a clock edge while signals are still X

SystemVerilog — sampling reset-window X values
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
covergroup cg @(posedge clk);                  // ← fires on cycle 0 too
    cp_state: coverpoint state;                // state is X during reset
endgroup
 
// On cycle 0, state is X.
// The auto-bin partition can include or exclude X (tool-dependent).
// X/Z values do not hit any wildcard bin (Module 11.5).
// The sample fires but counts go to either an X-bin (some sims) or
// nowhere (other sims). The bin counts now depend on the simulator.

Fix: add an iff (rst_n) qualifier to the sampling event so the covergroup only samples after reset has been deasserted: covergroup cg @(posedge clk iff rst_n);.

Pitfall 2 — Manual sample with stale argument values

SystemVerilog — manual sample on a stale handle
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
class env;
    apb_txn t;
    covergroup cg with function sample(apb_txn arg);
        cp_addr: coverpoint arg.paddr;
    endgroup
 
    task monitor();
        forever begin
            @(vif.pclk);
            cg.sample(t);                  // ← t may be the PREVIOUS transaction
                                            //   if the monitor hasn't published yet
        end
    endtask
endclass

Fix: make the sample call sequential with the transaction-commit logic. Sample after the monitor has reconstructed the current transaction, not on a clock edge that may precede the reconstruction.

Pitfall 3 — Event-driven covergroup that fires during scope-out

SystemVerilog — covergroup outlives its data scope
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
class txn;
    rand bit [31:0] addr;
    covergroup cg @(posedge top.clk);    // ← references signal outside class
        cp_addr: coverpoint addr;        // ← class field, may go out of scope
    endgroup
endclass
 
initial begin
    txn t = new();
    repeat (10) @(posedge top.clk);
    // t goes out of scope at the end of the initial block.
    // The covergroup keeps firing on every clock edge — sampling a dead
    // object's `addr` field. Behaviour is tool-specific (some sims hold
    // the reference, others crash).
end

Fix: prefer manual sampling for class-scope covergroups, or make the covergroup's lifetime explicit by destroying the class object deliberately. Event-driven covergroups in class scope are particularly fragile to object-lifecycle issues.

Pitfall 4 — iff filter that excludes the values the bins watch

This is the Module 11.6 debug-lab pattern, restated for completeness: a covergroup with @(posedge clk iff valid) samples only when valid is high. Any transition bin or cross bin whose values appear only when valid is low will silently read zero hits. The fix is to match the sampling-event filter to the values the bins are watching — or to acknowledge the filter in the bin's name and option.comment.

Debug Lab — "The Sample Fires, the Coverpoint Doesn't Update"

A subtle failure mode: the sampling event is firing reliably, but coverage bins still read zero. The investigation requires distinguishing four independent failure modes.

Buggy code
SystemVerilog — sample fires, bins stay empty
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
class env;
    rand bit [31:0] addr;
 
    covergroup cg with function sample(bit [31:0] addr_arg);
        cp_addr: coverpoint addr_arg {
            bins csr = {[32'h0000_0000 : 32'h0000_00FF]};
            bins ram = {[32'h1000_0000 : 32'h1FFF_FFFF]};
        }
    endgroup
 
    function new();
        cg = new();
    endfunction
 
    task monitor();
        forever begin
            @(vif.pclk iff vif.pready);
            // ── BUG: the monitor passes the testbench's randomized addr,
            //          but the DUT actually transferred a different value
            //          (a back-pressured re-issue, perhaps)
            cg.sample(this.addr);     // ← samples the *intended* address,
                                        //   not what the DUT actually transferred
        end
    endtask
endclass
Symptom

The regression runs cleanly. A $display confirms cg.sample() is being called once per APB transfer. The report shows cp_addr 0.0% — neither csr nor ram is being hit. The verification engineer believes the sampling is broken when it is actually working — the sample is firing, the bins are evaluating, but the sampled value never falls into the bin ranges.

Root cause

The monitor calls cg.sample(this.addr) — passing the testbench class's addr field. But this.addr was the value the testbench requested the transaction to use, not the value the DUT actually transferred on the bus. If the DUT did any address remapping, masking, or re-issue, the actual transferred address is on the interface signals, not in the testbench's field. The bins were drawn against bus-observed regions; the sampled values are testbench-internal — they may not even fall in the bin ranges at all (zero-filled or unset).

The structural mismatch is what is being sampled. The covergroup wants to measure what was observed on the bus, but the monitor is passing what was intended by the testbench. The two are not the same — and the bins (defined against bus observations) silently read zero because the values they see are from a different timeline.

Fix

Sample the observed bus value, not the intended testbench value. The monitor's job is to reconstruct what actually happened on the bus; that reconstruction is what the covergroup should sample.

SystemVerilog — sample the observed value, not the intended one
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
task monitor();
    bit [31:0] observed_addr;
    forever begin
        @(vif.pclk iff vif.pready);
        observed_addr = vif.paddr;          // ← sample bus directly
        cg.sample(observed_addr);
    end
endtask

The discipline rule that prevents recurrence: the sample argument is the observable. If the covergroup measures bus behaviour, sample the bus signals. If it measures transaction-class abstractions, sample the reconstructed transaction object. Never sample an upstream "intent" value when the verification scope is "what the DUT actually produced."

The complementary discipline: a directed sanity test before relying on the model. Drive a single transaction with a known address, single-step through the monitor, verify the sample value matches the bus observation. This costs five minutes; it catches the entire class of bug.

Industry Context — Where Sampling Strategy Pays Off

  • AMBA monitors. Production AXI / AHB / APB monitors invariably use the layered pattern shown above — interface-cycle coverage with @(posedge clk iff …), transaction-commit coverage with manual sample() driven by the protocol-decoder's commit detection. ARM's official AMBA VIPs publish the sampling-event documentation alongside each covergroup; integrators read both the bin definitions and the sampling rules to understand the coverage semantics.
  • DDR / LPDDR controllers. Coverage on a memory controller typically uses three or four covergroups, each with its own sampling event: command-bus coverage at @(posedge ck), refresh interaction at @(refresh_committed), training-step coverage at @(training_step_advance). The multi-event pattern mirrors the spec's clock/event hierarchy.
  • UVM monitor → analysis port → coverage subscriber pattern. UVM's canonical coverage shape is monitor → analysis_port → subscriber.write(t) → cg.sample(t). The write() call is the manual sample trigger; the transaction t is the sample argument. Every UVM-style covergroup that follows this pattern uses with function sample(transaction_t arg).
  • Formal / hybrid flows. When formal property verification (Module 12 + JasperGold / Cadence Jasper) runs alongside simulation coverage, the formal engine's "cover witnesses" can be exported as simulation samples — fed to the simulation coverage model via manual sample() calls with the witness values. This bridges formal-found scenarios into the simulation coverage database, closing gaps that random simulation could not reach.
  • Coverage merging across test groups. Multi-test regressions in CI flows merge coverage databases; the sampling discipline determines whether the merge is semantically meaningful. Two tests sampling the same covergroup with different iff conditions produce databases that cannot be sensibly merged — the type-level merged number conflates two different metrics. Production discipline keeps the sampling event identical across every instance of the same covergroup type; per-instance differences live in the structural bins, not in the sampling event.

The pattern across these uses: the sampling event is the interface between the testbench's measurement intent and the simulator's data collection. Get the interface wrong and every downstream coverage report inherits the mismatch.

Interview Q&A — Ten Questions You Will Be Asked

Event-driven (covergroup cg @(event);) and manual (covergroup cg; with no header event, then cg.sample(); calls in the testbench). Event-driven sampling is the simulator's responsibility — it fires automatically at the declared event. Manual sampling puts the testbench in control of when the sample happens, which is the right choice for transaction-level coverage where the "right moment" is a software event the monitor controls.

Coding Guidelines — Seven Rules for Sampling-Event Discipline

  1. Match the sampling event to the verification layer. Signal-level coverage → @(posedge clk). Bus-level coverage → @(posedge clk iff bus_qualifier). Transaction-level coverage → manual cg.sample(t). Mismatched layer choice produces silently misleading reports.
  2. Document the sampling event in option.comment. State which spec moment the sample corresponds to. The auditor reading the report should not need access to the source to understand when a sample fired.
  3. Use with function sample(...) for transaction-driven coverage. Pass the reconstructed transaction object as argument; let the covergroup's coverpoints read fields from the argument. This decouples the covergroup from the testbench's lexical scope and matches the UVM monitor → analysis-port → subscriber pattern.
  4. Add iff rst_n to clock-edge sampling events. Bare @(posedge clk) samples during reset windows where signals are X — the bin counts then depend on the simulator. Qualifying the event excludes the X-sample window cleanly.
  5. Sample the observable, not the intent. Bus coverage samples bus signals (or the monitor's reconstruction of them). Don't sample the testbench's randomization output — the DUT may have produced a different value, and the bin definitions were drawn against the observed value.
  6. Keep the sampling event identical across instances of the same covergroup type. Different sampling rules on different instances conflate metrics at merge time; the type-level number stops meaning anything reproducible.
  7. Treat covergroup sampling-event edits as semantic changes. Code review should question any covergroup cg @(...) line change with the same scrutiny it gives a bin-definition edit. The sampling event is part of every transition and cross bin's contract; changing it silently changes the meaning of the report.

Summary

The sampling event determines when the simulator freezes the coverpoint expressions and updates the bin counters. Event-driven sampling (@(event)) is the simulator's responsibility — useful for signal-level and bus-level coverage at clock edges. Manual sampling (cg.sample()) puts the testbench in control — the right form for transaction-level coverage where the right moment is a software event. with function sample(...) lets the covergroup take arguments at sample time, the canonical pattern for monitor-driven coverage. The sampling event runs in the Postponed region, seeing settled values; sub-cycle properties need SVA. The sampling event is part of every transition and cross bin's contract, and code review should treat changes to it as semantic, not stylistic. The three-layer pattern (signal / bus / transaction) is the right shape for non-trivial environments. Module 11.10 closes the module with instance-vs-type coverage and the merging semantics that make multi-instance, multi-regression coverage tractable at sign-off.