Skip to content
STIMSMITH

SOURCE ARCHIVE

SHA256: aebb9ad86b5471709880f623182cf96f8b65aad85dd9aaec4fe4125a0bedde1f
TYPE: text/html
SIZE: 74.5 KB
FETCHED: 7/14/2026, 10:31:57 AM
EXTRACTOR: http-html
CHARS: 9,580

EXTRACTED CONTENT

9,580 chars

When verifying a digital design, one of the core challenges is generating meaningful stimulus — inputs that exercise the design the way it will be used in the real world. Feeding in completely arbitrary, unconstrained random values might technically "randomize" your test, but it often results in a flood of illegal or uninteresting scenarios while missing the interesting corner cases that matter. SystemVerilog constraints solve exactly this problem: they let you define the boundaries of legal behavior and let the simulator's built-in solver figure out what values to generate within those boundaries.

This article covers what constraints are, why they exist, how they fit into the broader verification flow, and how to write your first constraint blocks — stepping beyond the absolute basics into patterns you'll actually use on real projects.

Direct Tests vs Randomized Tests

Before diving into constraints themselves, it helps to understand the two broad strategies for writing verification tests.

What are direct tests ?

A direct test is fully hand-crafted. You explicitly set every signal to every value, in the exact sequence you want. For a simple UART transmitter, a direct test might look like:

// Direct test: manually drive a specific transaction
	pkt.addr = 8'h10;
	pkt.data = 8'hAB;
	pkt.cmd  = WRITE;

Direct tests are fast to write for well-understood scenarios and are great for checking a specific known bug. However, they do not scale. A moderately complex design might have hundreds of valid configurations. Writing a direct test for every combination is impractical, and it's very easy to miss something.

What are randomized tests ?

A randomized test relies on the simulator to generate values automatically. The test is run repeatedly with different seeds, so each run exercises a different slice of the design's behavior:

// Randomized test: let the solver pick the values
assert(pkt.randomize());

The key insight is that randomized tests, over many runs, will naturally exercise corner cases that a human engineer simply wouldn't think to write explicitly. This is why constrained-random verification is the dominant methodology in modern chip verification flows, including industry frameworks like UVM (Universal Verification Methodology).

The catch is the word constrained. Purely unconstrained randomization is noisy — you'll spend most of your simulation budget on invalid or uninteresting inputs. Constraints are how you guide the randomizer to stay in bounds.

What is a Constraint ?

A constraint is a declaration that narrows the space of valid random values for one or more variables. You define constraints inside a class, alongside the variables they govern. The SystemVerilog simulator includes a constraint solver — an engine that reads all active constraints and picks values that satisfy all of them simultaneously before assigning them to your variables.

The simplest form looks like this:

class Packet;
    rand bit [7:0] addr;
    rand bit [7:0] data;

    constraint addr_range { addr inside {[8'h00 : 8'h7F]}; }
endclass

A few things to notice here:

  • rand keyword: Marking a variable rand tells the solver it is a candidate for randomization. Without this, randomize() ignores the variable entirely.
  • constraint block: Named with a label (addr_range), the constraint body is an expression that must evaluate to true. The solver finds values that make it true.
  • inside operator: This is a range operator that restricts addr to the lower half of the 8-bit range (0–127) although full range is 0-255. You'll see this operator frequently in practice.

The rand and randc Keywords

SystemVerilog provides two modifiers for random variables:

rand - Uniform Random

Variables declared with rand are assigned uniformly random values within the constraint space on each call to randomize(). Each call is independent — you might get the same value twice in a row.

rand bit [3:0] opcode;   // any value 0–15, independently each time

randc — Random Cyclic

Variables declared with randc cycle through all possible values before repeating any. Think of it like drawing cards from a shuffled deck without replacement — you won't see a repeat until every value has been generated once.

randc bit [3:0] opcode;  // cycles through 0–15 before repeating

randc is particularly useful when you want guaranteed coverage of all values, not just statistically likely coverage. It's a subtle but important distinction when verifying things like register address spaces where every address must be hit.

Writing a Constraint Block

A constraint block has the following structure:

constraint <name> { <expression>; }

Multiple expressions can live inside a single block, and they are treated as a logical AND — all must be satisfied:

class BusTransaction;
    rand bit [7:0]  addr;
    rand bit [31:0] data;
    rand bit [1:0]  burst_len;

    // addr must be 4-byte aligned
    constraint align_addr { addr[1:0] == 2'b00; }

    // burst_len between 1 and 4
    constraint valid_burst { burst_len >= 1; burst_len <= 4; }
endclass

You can also write multiple separate constraint blocks. Keeping constraints logically separated makes them easier to enable, disable, and override in subclasses — which becomes important as your testbench grows.

Calling randomize()

Constraints only come into play when you call the randomize() method on a class object. This method invokes the solver, generates new values, and returns 1 (success) or 0 (failure — meaning no valid combination existed).

Always check the return value. A failing randomize() is a silent killer in testbenches — if you ignore the return value, your variables will hold stale data from the previous call and you'll have no idea.

Packet pkt = new();

// Correct: assert will trigger a fatal if randomize fails
assert(pkt.randomize()) else $fatal(1, "Randomization failed!");

If the solver cannot find any values that satisfy all constraints simultaneously, randomize() returns 0. This typically means your constraints are over-specified or contradictory. For example:

constraint impossible { addr > 8'hFF; }  // addr is 8-bit, can never exceed 0xFF

The solver would fail immediately on this.

Constraint Expressions: A Quick Tour

Here are the most common types of expressions you'll use within constraints:

Relational Constraints

constraint c1 { data < 32'h1000; }
constraint c2 { addr >= 8'h10; addr <= 8'hF0; }

The inside Operator

Restricts a variable to a set or range of values:

constraint c3 { opcode inside {READ, WRITE, IDLE}; }
constraint c4 { size inside {[4:64]}; }

You can also negate it with !:

constraint c5 { !(opcode inside {RESERVED_1, RESERVED_2}); }

Weighted Distributions with dist

The dist operator controls the probability weighting of different values. This is useful when you want a biased distribution — for example, you want read transactions 70% of the time:

constraint cmd_dist {
    cmd dist { READ := 70, WRITE := 30 };
}

Use := to assign equal weight to each listed item individually, or :/ to split a weight across a range:

// Each value in 0–7 gets weight 10; value 8 gets weight 50 (by itself)
constraint priority_dist {
    priority dist { [0:7] :/ 10, 8 := 50 };
}

Implication Constraints

Sometimes a constraint only applies when another condition is met. The -> implication operator handles this:

constraint read_no_data {
    (cmd == READ) -> (data == 0);
}

This reads: "if cmd is READ, then data must be 0. If cmd is WRITE, the constraint on data doesn't apply."

Enabling and Disabling Constraints

Every named constraint block can be turned on or off at runtime using constraint_mode():

pkt.addr_range.constraint_mode(0);  // disable
pkt.addr_range.constraint_mode(1);  // re-enable

This is extremely useful when the same transaction class is reused across many test scenarios. You can write all possible constraints in the class and selectively activate them per test, rather than creating separate subclasses for every variation.

Inline Constraints with randomize() with

Sometimes you want to add a one-off constraint for a single randomize() call without modifying the class. Use the with syntax:

assert(pkt.randomize() with { addr == 8'hFF; });

The inline constraint is applied on top of all existing class constraints for that one call only. It's a clean way to specialize behavior in a specific test scenario without polluting the base class.

Summary

  • Constraints let you define the legal value space for random variables, so stimulus is realistic rather than arbitrary.
  • Always use rand or randc on any variable you want randomized; without it, randomize() ignores the variable.
  • Always check the return value of randomize() — a failed solve silently leaves variables unchanged.
  • Named constraint blocks can be disabled and re-enabled at runtime with constraint_mode(), giving you flexible reuse across test scenarios.
  • Inline constraints (randomize() with { ... }) are the right tool when you need a one-time override without changing the class.
  • The constraint solver handles all expressions simultaneously — you don't need to worry about ordering, just correctness.

The subsequent articles in this series dive deeper into each of these features: constraint inheritance, solve...before ordering hints, foreach constraints for arrays, soft constraints, and more.