Consider verifying an AXI interconnect that connects eight master ports to four slave ports, each transaction carrying a burst type (FIXED, INCR, WRAP), a burst length (1–16 beats), a transfer size (1–64 bytes), a protection attribute, and a transaction ID. The interesting scenarios are not individual address values — those are checked by range — but the combinations: what happens when multiple masters issue overlapping bursts simultaneously, when a WRAP burst hits a region boundary, or when a high-priority transaction and a low-priority transaction contend for the same slave port at the same time?

The number of meaningful multi-master interaction scenarios grows faster than any directed test suite can cover. Constrained random verification solves this by letting a constraint solver generate legal, varied transactions automatically, while coverage measurement tracks which scenario combinations have actually been exercised.

12 min read | Intermediate Level

What You'll Learn

  • What constrained random verification is and why directed testing alone cannot scale to real designs
  • How the constraint solver, randomization seed, and coverage model work together
  • How to write a randomized transaction class with constraints in SystemVerilog
  • The CRV feedback loop and when CRV struggles

Why Pure Random Testing Is Not Enough

The most naive form of randomization — $random with no constraints — generates inputs with no regard for legality. For a design with a handshake protocol, unconstrained random stimulus will constantly violate the protocol, producing failures that reveal nothing about the design and flooding the log with irrelevant errors. For a design with an AXI interface, a random burst length of zero or an unaligned address may be illegal by specification — testing those scenarios tells you nothing useful about the RTL.

Constrained random verification (CRV) addresses this by combining two ideas:

  • Randomization — stimulus values are generated by a solver, not handcrafted, so they explore a much larger space than any engineer could write manually
  • Constraints — the solver only generates stimulus that satisfies a set of rules, ensuring every generated transaction is legal according to the design specification

The result is a testbench that automatically produces thousands of legal, varied transactions per regression run — each one a potential bug finder that a directed test author would never have thought to write.

CRV in Practice

A verification environment for a PCIe controller might define a transaction class with random fields for TLP type, payload length, address, byte enables, and traffic class. Constraints ensure that only valid TLP type and length combinations are generated, that addresses are aligned to their transfer size, and that reserved fields stay zero. Running 10,000 randomized transactions with a self-checking scoreboard explores a vastly larger scenario space than any directed test suite — and does so automatically, overnight, without additional engineer effort per run.

The CRV Feedback Loop

CRV is not a one-shot activity. It is a closed-loop process that runs iteratively until coverage targets are met:

  1. Generate — the constraint solver produces randomized transactions that satisfy the defined constraints
  2. Simulate — the testbench drives the transactions into the DUT; a scoreboard checks that outputs match expected values
  3. Collect coverage — the simulator records which functional coverage points were hit during this run
  4. Analyze gaps — the verification engineer reviews the coverage report to identify scenarios that have not been exercised
  5. Tighten constraints or add directed tests — new constraints are written to bias the solver toward the uncovered areas, or a directed test forces the specific scenario
  6. Repeat — the loop continues until all coverage targets are met

Coverage closure is rarely achieved by running more random seeds alone. The final 5–10% of functional coverage typically requires deliberate constraint tightening or targeted directed tests. The CRV feedback loop is what makes this systematic rather than guesswork.

Key Concepts

The Constraint Solver

When randomize() is called on a SystemVerilog object, the simulator's built-in constraint solver finds a set of random values for all rand variables that simultaneously satisfies every active constraint. The solver handles complex inter-variable relationships — if one constraint says addr % transfer_size == 0 (address alignment) and another says transfer_size inside {1,2,4,8}, the solver finds values for both that satisfy both rules together. If no valid solution exists — because the constraints are over-specified or contradictory — randomize() returns 0 (failure).

The Randomization Seed

The constraint solver uses a pseudo-random number generator seeded by a value called the randomization seed. Given the same seed, the same constraints, and the same simulator, randomize() produces the exact same sequence of values every time. This is essential for reproducibility: when a random test finds a bug, the seed is recorded so the exact failing scenario can be replayed.

Always log the randomization seed at the start of every simulation run. When a nightly regression finds a bug, you need that seed to reproduce it. Most simulators provide a +ntb_random_seed or equivalent plusarg for this.

Functional Coverage

Without a way to measure what the random stimulus has actually exercised, CRV is just generating noise. Functional coverage — defined using SystemVerilog covergroup and coverpoint constructs — tracks which design-level scenarios have been hit during simulation. Coverage closure means every planned scenario has been observed at least once. See Functional Coverage for a deep dive into covergroups and coverpoints.

Writing a Randomized Transaction in SystemVerilog

In a CRV environment, stimulus is represented as a transaction object — a SystemVerilog class whose fields are declared rand so the solver can randomize them. Constraints are written inside the class as constraint blocks.

The example below shows a transaction for a simple memory interface. The DUT accepts read and write requests with an address, a data payload (for writes), and a byte-enable mask. The constraints ensure only legal combinations are generated:


// Transaction class for a simple memory interface
class mem_transaction;

    // --- Randomized fields ---
    rand bit        write;           // 1 = write, 0 = read
    rand bit [31:0] addr;            // byte address
    rand bit [31:0] wdata;           // write data (ignored for reads)
    rand bit [3:0]  byte_en;         // byte enable: which bytes are active

    // --- Constraint: address must be word-aligned (bits [1:0] always 0) ---
    constraint c_align {
        addr[1:0] == 2'b00;
    }

    // --- Constraint: address must be within the mapped region ---
    constraint c_addr_range {
        addr inside {[32'h0000_0000 : 32'h0000_FFFF]};
    }

    // --- Constraint: byte enables must have at least one active byte ---
    constraint c_byte_en {
        byte_en != 4'b0000;
    }

    // --- Constraint: for reads, byte enables must be all-ones (full-word read) ---
    // This is a conditional constraint: only active when write == 0
    constraint c_read_be {
        if (!write) byte_en == 4'b1111;
    }

endclass

In a testbench, the transaction is created, randomized, and driven into the DUT:


module tb;

    mem_transaction tr;

    initial begin
        tr = new();

        repeat (1000) begin
            // randomize() returns 1 on success, 0 if constraints cannot be solved
            if (!tr.randomize())
                $fatal(1, "Randomization failed — check for conflicting constraints");

            // Drive the randomized transaction into the DUT
            // (actual signal driving omitted — handled by a driver in a UVM env)
            $display("op=%s addr=0x%08h be=%04b",
                     tr.write ? "WR" : "RD", tr.addr, tr.byte_en);
        end
    end

endmodule

Only inputs to the DUT should be declared rand — signals that the DUT drives as outputs are never randomized. The constraint solver generates stimulus; the DUT generates the response. A common beginner mistake is to declare output signals as rand and try to constrain them to match expected results — this bypasses the DUT entirely.

Inline Constraints with randomize() with

Constraints defined inside the class apply every time randomize() is called. Sometimes a specific test needs to add a temporary constraint for one randomization call without changing the class. The randomize() with syntax adds an inline constraint that applies only for that call:


// Standard randomization — uses only class constraints
tr.randomize();

// Inline constraint: force this specific call to generate a write to the boot region
tr.randomize() with {
    write   == 1;
    addr inside {[32'h0000_0000 : 32'h0000_00FF]};
};

// Inline constraint: generate a narrow (single-byte) write
tr.randomize() with {
    write   == 1;
    byte_en inside {4'b0001, 4'b0010, 4'b0100, 4'b1000};
};

This pattern is widely used during coverage closure: once the coverage report shows that a specific scenario has not been hit, an inline constraint targets that exact scenario without requiring changes to the transaction class.

Common Mistakes in CRV

Mistake #1: Constraints that are too tight

Wrong: constraint c_addr { addr == 32'h1000; } — fixes the address to one value
Right: constraint c_addr { addr inside {[32'h1000:32'h2000]}; } — allows a range

Why? Over-constrained stimulus defeats the purpose of randomization. If all generated transactions have the same address, you are running thousands of copies of one directed test. Constraints should define the legal space, not pin specific values.

Mistake #2: Not checking the return value of randomize()

Wrong: tr.randomize(); — ignores failure
Right: if (!tr.randomize()) $fatal(1, "Randomization failed");

Why? If randomize() fails due to conflicting constraints, it returns 0 and leaves the object's fields unchanged from their previous values. The testbench then silently drives the same stimulus repeatedly, producing meaningless results. Always check the return value.

Mistake #3: No functional coverage model

Wrong: Running 50,000 random transactions and declaring verification complete
Right: Defining a functional coverage model and running until all coverage points are closed

Why? Without coverage, you have no way to know which scenarios the random stimulus actually exercised. A simulation that ran 50,000 transactions may have never generated a write to the highest address in the mapped region, a single-byte enable pattern, or a read immediately following a write to the same address. Coverage is what turns random activity into measurable progress.

Quick Check

Q: A testbench runs 10,000 random transactions and the simulation passes. Is the design verified?

Show Answer

Not necessarily. Passing means the DUT produced correct outputs for those 10,000 transactions — but without a functional coverage model, there is no way to know which scenarios were actually exercised. Key corner cases — boundary addresses, specific byte enable combinations, back-to-back write-read to the same address — may never have appeared in those 10,000 transactions. Verification confidence in CRV comes from coverage closure, not from transaction count or a green pass rate.

When CRV Excels and When It Struggles

Where CRV Works Best

  • Wide stimulus spaces — designs with large address ranges, wide data buses, or many control signal combinations that cannot be enumerated manually
  • Independent transactions — interfaces where each transaction is largely independent of the previous one, making random ordering natural and legal
  • Finding unexpected interactions — CRV explores combinations the engineer never thought to test, which is precisely where bugs in complex designs tend to hide
  • Regression scalability — running the same constrained environment with thousands of different seeds overnight, each exploring a different region of the state space

Where CRV Struggles

  • Deeply sequential protocols: Some scenarios require a precise sequence of many dependent events to set up. For example, reaching a specific state in a multi-phase cache coherence protocol may require dozens of prior transactions in a specific order. The probability of a random generator naturally producing that sequence is extremely low, even after millions of seeds. These scenarios are better handled with directed tests or heavily biased constraints.
  • Rare corner cases with tight conditions: A bug that only triggers when two independent blocks simultaneously reach their maximum count on the same clock cycle may never be hit by unconstrained randomization. Coverage-directed generation — using the coverage model to steer stimulus toward unhit scenarios — helps, but sometimes a directed test is the only practical solution.
  • Performance-sensitive scenarios: Measuring maximum throughput or worst-case latency requires specific, controlled traffic patterns rather than random stimulus. Directed tests or constrained scenarios with fixed structure are more appropriate.

CRV and directed testing are not competing approaches — they cover different risks. CRV finds the unexpected; directed tests guarantee the known. A mature verification environment uses both: CRV for the bulk of scenario exploration, and directed tests for protocol corner cases, coverage hole closure, and bug regression.

Quick Check

Q: After 5,000 random seeds, a functional coverage point for "back-to-back writes to the same address" remains uncovered. What is the most efficient next step?

Show Answer

Add a constraint — either inline with randomize() with or as a dedicated test — that forces two consecutive write transactions to generate the same address. Running more unconstrained random seeds is unlikely to help: if 5,000 seeds have not hit it, the probability under current constraints is very low. A targeted constraint closes the gap in one run rather than waiting for a lucky seed.

Practice Exercise

Challenge: Add Constraints to a Burst Transaction

The transaction class below represents a burst memory transfer. It is missing its constraints. Add constraints that enforce the following rules from the design specification:

  1. Burst length must be 1, 2, 4, or 8 beats
  2. Start address must be aligned to the burst size in bytes (e.g., a 4-beat burst with 4-byte beats must start at an address divisible by 16)
  3. Burst type must be INCR (1) or WRAP (2) — FIXED (0) is not supported by this DUT
  4. For WRAP bursts, length must be exactly 2, 4, or 8 (not 1)

class burst_transaction;

    rand bit [31:0] start_addr;
    rand bit [3:0]  burst_len;    // number of beats: 1, 2, 4, or 8
    rand bit [1:0]  burst_type;   // 0=FIXED, 1=INCR, 2=WRAP
    rand bit [1:0]  beat_size;    // bytes per beat: 0=1B, 1=2B, 2=4B, 3=8B

    // Add your constraints here

endclass
Hint

For address alignment, the start address modulo the total burst size in bytes must equal zero. The total burst size in bytes is burst_len * (1 << beat_size). Use a conditional constraint for the WRAP-specific length restriction.

Solution

class burst_transaction;

    rand bit [31:0] start_addr;
    rand bit [3:0]  burst_len;
    rand bit [1:0]  burst_type;
    rand bit [1:0]  beat_size;

    // Rule 1: legal burst lengths only
    constraint c_len {
        burst_len inside {1, 2, 4, 8};
    }

    // Rule 2: start address aligned to total burst byte size
    constraint c_align {
        start_addr % (burst_len * (1 << beat_size)) == 0;
    }

    // Rule 3: FIXED bursts not supported
    constraint c_type {
        burst_type inside {1, 2};
    }

    // Rule 4: WRAP bursts cannot have length 1
    constraint c_wrap_len {
        if (burst_type == 2) burst_len inside {2, 4, 8};
    }

endclass

Summary

In this tutorial, you learned:

  • CRV combines constrained stimulus generation with coverage measurement to systematically explore a design's state space
  • The constraint solver generates legal random values for rand variables; constraints define what "legal" means for the design under test
  • The randomization seed makes runs reproducible — always log it so failing scenarios can be replayed
  • The CRV feedback loop is: generate → simulate → collect coverage → analyze gaps → tighten constraints → repeat
  • randomize() with adds inline constraints for a single call, useful for targeting specific coverage holes without modifying the transaction class
  • CRV excels at wide stimulus spaces and finding unexpected interactions; it struggles with deeply sequential protocols and rare timing coincidences, which are better handled with directed tests
Did you find what you were looking for?

Quiz

No quiz questions available for this article.