Skip to content
DenseChip
Learn
PracticeCode labInterview

DenseChip

Navigate

Menu

LearnPracticeCode labInterview

Tools

All toolsSV editorCode repoResume
DenseChip

Practice, courses, and chip-design tools in one focused workspace.

LearnPracticeCode labInterviewToolsLearning paths

© 2026 DenseChip

AboutContactPrivacyTerms
Skip to lesson

Course contents

  • Start Here6
  • SystemVerilog Language Foundations28
  • Object-Oriented SystemVerilog for Verification30
  • Constraint Randomization — Interview-Critical Deep Dive36
  • SystemVerilog Assertions — Interview-Critical Deep Dive40
  • Functional Coverage in SystemVerilog22
  • Testbench Architecture in SystemVerilog23
  • Advanced Language & Integration25
  • Senior & Interview Preparation21
  • Principal-Level Verification28
    • Reusable Testbench Architecture
    • Checking Out-of-Order Traffic
    • Assertion Depth
    • Coverage Modelling for Real Features
      • Transaction and Temporal Coverage
      • Coverage Filters
      • Assertion-Backed Coverage
      • Closure at Scale
    • Capstone: Out-of-Order Memory Controller
    • Principal Judgment
Loading lesson

‹Coverage Modelling for Real Features
  1. Topics
  2. SystemVerilog
  3. Principal-Level Verification
  4. Coverage Modelling for Real Features
  5. Assertion-Backed Coverage

Part 9 · Principal-Level Verification · Advanced

Assertion-Backed Coverage

Use cover property for temporal plan rows, read the hit counts back and check them, and find the hole.

On this page

    Some plan rows are sequences

    "A beat waited exactly two cycles, then was accepted." "Three beats were accepted back to back." These rows describe a shape in time. A covergroup can bin a number computed by a monitor, but writing a monitor state machine for every shape is slow and easy to get wrong. A cover property states the shape directly, in the same language as the assertions that check the protocol. Directives introduces cover; this lesson makes covers carry plan rows.

    What a cover counts

    A cover property starts an evaluation attempt on every clock, like an assertion. It counts at most one success per attempt and reports vacuous successes in a separate count. By default the action block also runs on a vacuous success (20.12), so avoid implication inside a cover, or turn vacuous passes off with $assertcontrol (NonvacuousOn) or $assertvacuousoff. A plain sequence, like every cover in this lesson, has no vacuous success. A cover sequence instead counts every match of every attempt (IEEE 1800-2017, 16.14.3). Two practical rules follow.

    • Write each scenario so one attempt can match in only one way, or the count depends on the form of cover you used. stall [*2] has one match per attempt; stall [*1:3] has up to three.

    • Anchor the start. (valid && ready) [*3] started on every cycle counts a run of four transfers twice. That is fine if the plan row means "windows of three", wrong if it means "runs of three".

    Add disable iff (!rst_n) so reset does not produce or abort hits, and keep covers on sampled signals only, the same as assertions.

    Linking covers to plan rows

    The plan has rows with IDs; the coverage report has item names. The link is the name. Label each cover with its row ID, and have the action block count hits per row in an associative array keyed by the ID. Vendor plan tools (for example VCS hierarchical verification plans, Questa test-plan tracking and Cadence vPlan) import a spreadsheet and map each row to coverage items by name, so a consistent label is what makes the report readable at a review.

    Row

    Scenario

    Cover

    P3.1

    Accept with no wait after idle

    $rose(valid) && ready

    P3.2

    Exactly two stall cycles, then accept

    ~stall ##1 stall [*2] ##1 xfer

    P3.3

    Three accepts in consecutive cycles

    xfer [*3]

    P3.4

    A stall longer than three cycles

    stall [*4]

    A bench that reads the hits back

    The DUT is a sink that stalls for data[1:0] cycles after each accepted beat, so the stimulus chooses the stall lengths. The bench has two counters per row: one filled by the cover action blocks and one by a procedural mirror built from a four-cycle history of sampled values. It checks the mirror against hand-counted expected hits and, when the tool evaluated the covers, checks the covers against the mirror.

    A tool fact decides that last clause. Verilator 5.048 evaluates cover property only when the model is built with --coverage-user (or --coverage, which includes it). Without that flag the covers compile but never run, and their action blocks never execute; this was confirmed by running the bench both ways. The course runner does not pass the flag, so on this site only the procedural mirror is checked: a wrong cover still passes here. To check the covers too, build it yourself with verilator --binary --timing --assert --coverage-user tb_plan_covers.sv and run obj_dir/Vtb_plan_covers. A sentinel cover, c_alive, tells the bench which case it is in: if even a cover on rst_n never hit, covers were not evaluated in this build.

    busy_sink.sv
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    // Sink that stalls for data[1:0] cycles after each accepted beat.
    module busy_sink (
      input  logic       clk,
      input  logic       rst_n,
      input  logic       valid,
      input  logic [7:0] data,
      output logic       ready
    );
      timeunit 1ns; timeprecision 1ps;
      logic [1:0] busy;
      assign ready = (busy == 2'd0);
      always_ff @(posedge clk or negedge rst_n)
        if (!rst_n)                busy <= 2'd0;
        else if (valid && ready)   busy <= data[1:0];
        else if (busy != 2'd0)     busy <= busy - 2'd1;
    endmodule
    

    The hand count follows from the stream. P3.1 hits at beat 1 and at beat 7, the two beats that follow idle cycles. P3.2 hits once, at beat 4, which waits two cycles behind beat 3; beat 6 waits three cycles and does not match. P3.3 hits at beats 1 to 3 and twice in the run of four, beats 7 to 10. P3.4 needs four stall cycles and the DUT never stalls more than three, so it stays at 0.

    Built without the flag, as the course runner builds it, the run prints the note and checks the mirror alone. Built with --coverage-user, each cover count equals the mirror. Two mutants show the difference. A DUT that ignores data and never stalls drops P3.2 to 0 and raises P3.3 to 6 in the mirror, so it fails in both builds. Changing the P3.3 cover to xfer [*2] makes it report 6 against the mirror's 3, but only the --coverage-user build sees that; the course runner still prints PASS.

    Holes that cannot be filled

    P3.4 is a hole by design. The stall field is two bits, so no stimulus can make the sink wait four cycles. Three answers are possible, and the plan must record which: remove the row because the spec caps stalls at three; keep it and reconfigure the DUT so long stalls happen; or ask a formal tool to prove the cover unreachable and waive it with that proof. A cover that is reachable in the spec but unreachable in the bench is the dangerous case, because it usually means the stimulus is too narrow.

    A cover's action block can also feed a covergroup. Calling cg.sample(stall_len) from a cover that ends at the accept bins the stall lengths that actually occurred, which joins a temporal shape to a value distribution.

    Common pitfall

    Reading a zero-hit cover as a stimulus gap without first checking that covers ran at all. A build flag, $assertoff, or a disable iff stuck high gives every cover zero. A sentinel cover that must always hit separates "not reached" from "not evaluated".

    Interview

    Question: "How do you know your cover properties are measuring what the plan says?" Answer: label each with its plan row, write the scenario so each attempt matches one way, check the counts once against a directed test with known hits, and keep a sentinel cover so a disabled build cannot report holes that are not real.

    Check yourself A run of four consecutive transfers occurs once. How many times does cover property (@(posedge clk) xfer [*3]) count it, and why?

    Twice. An attempt starts on every clock; the attempts starting at the first and second transfer of the run each see three transfers. Each attempt counts at most once, so a run of four gives two hits.

    Check yourself Every cover in a regression reports 0 hits, including trivial ones. What do you check first?

    Whether covers were evaluated: the build options (Verilator needs --coverage-user), assertion control calls such as $assertoff, and the disable iff condition. A sentinel cover on an always-true condition answers this in one line of the report.

    Related topics

    • Previous: Coverage Filters

    • Next: Closure at Scale

    • assert, assume, cover, restrict & Action Blocks

    Previous

    248 / 259

    Next: Closure at Scale

    Up next

    Up nextClosure at Scale

    Sign in to save progress

    Practice this lessonReport an error

    See also

    • Transaction and Temporal Coverage