Skip to content
STIMSMITH

Constrained-Random Stimulus Generation

Concept WIKI v2 · 6/9/2026

Constrained-random stimulus generation is a directed-random verification technique in which a solver automatically selects values for randomized fields of a data object subject to a set of Boolean constraints, producing legal stimuli with controlled distributions. In SystemVerilog, the technique is built on rand class members, the `constraint` keyword, the implicit `randomize()` method, and the optional `with` clause for inline constraints; in industrial practice (e.g. the AMD/Synopsys microcode generator) it is implemented as a hierarchical, knob-driven opcode generator profiled with tools such as the VCS constraint profiler.[^2119d9e0][^6fa7d584][^5204399e][^4de14aa6][^78097f25]

Constrained-Random Stimulus Generation

Constrained-random stimulus generation is a directed-random verification technique in which a constraint solver automatically selects values for randomized fields of a data object subject to a set of Boolean constraints, producing legal stimuli with controlled distributions. In SystemVerilog it is the central mechanism of the directed-random verification methodology, and in industrial practice (such as the AMD/Synopsys microcode work) it is realized as a hierarchical, knob-driven opcode generator.[1][2][3]

Directed-random verification context

Traditional simulation-based verification has used a directed testing approach in which a testbench applies specific data values. A purely random approach can find more errors but, unless simulations run for very long periods, may still miss certain problems. A directed-random test controls how random the data values are using constraints, for example by ensuring that "corner cases" such as minimum and maximum addresses are definitely tested, or that some memory locations are tested exhaustively.[1]

SystemVerilog supports directed, random, and directed-random testing by providing random data value generation under the control of constraints. To measure how good a test is, SystemVerilog provides constructs for specifying functional coverage models and measuring coverage during simulation; coverage data can then be used to direct subsequent tests.[1]

Class-based data model

Most practical verification problems involve transactions in which a collection of data is transferred into or out of the design under test (DUT). It is appropriate to create an abstract data structure that can be used to represent this information as it moves through the verification system and the DUT. Typical transactions range from a simple address/data bus transfer to a complete image represented as video data.[1]

The Doulos CANbus tutorial illustrates the canonical class-based data model. A packed message_t struct holds the message fields (an 11-bit identifier, an RTR bit, two reserved bits, a 4-bit DLC, a dynamic data payload, and a 15-bit CRC), with the fields intended to be randomized marked rand. A class CAN_Message then wraps the struct as a rand member, so that functions can be associated with the data and built-in callbacks such as pre_randomize() and post_randomize() become available.[1][2]

typedef struct {
  rand bit [10:0] ID;
  rand bit        RTR;
       bit  [1:0] rsvd;
  rand bit  [3:0] DLC;
  rand byte       data[];
       bit [14:0] CRC;
} message_t;

class CAN_Message;
  rand message_t message;
  // class methods here
endclass: CAN_Message

Class methods can directly access the rand fields using .member notation and can be used to maintain invariants — for example, a set_RTR task that clears the payload and sets DLC to zero when the RTR bit is asserted.[2]

Constraints as Boolean expressions

A constraint is a Boolean expression describing some property of a field. Constraints direct the random generator to choose values that satisfy the properties expressed in the constraints; within the limits of the constraints, the values are still randomly chosen. The process of choosing values that satisfy the constraints is called solving, and the verification tool that does it is called the solver. The solver may be embedded in a simulator or be part of a separate testbench generator program.[2]

The inside range-membership operator is the canonical way to express a numeric range constraint, e.g. DLC inside {[0:8]} to restrict the 4-bit DLC field to the legal CANbus data-length range.[2] Constraints are class members, just like fields and methods, and can be written either in the original class or in derived classes.[4]

The size of a dynamic array can be tied to another field within a constraint, e.g. message.data.size() == message.DLC, so that the payload length and DLC are always consistent.[4]

Conflicting constraints

It is sometimes possible to write conflicting constraints, in which case the generator will fail. Constraint solvers therefore must either detect inconsistency or fail to find a satisfying assignment.[4]

Calling randomize() in the testbench

Once a class is defined, a testbench creates one or more objects and calls randomize() on each. The Doulos example builds a 10-element unpacked array of CAN_Message objects and randomizes each entry inside a generation loop, also using std::randomize() with a with clause to generate auxiliary values such as inter-message intervals.[4]

CAN_Message test_message[10];
for (int i = 0; i < 10; i++) begin
  std::randomize(interval) with { interval>0; interval<6; };
  test_message[i] = new;
  test_message[i].randomize();
  test_message[i].print();
  test_message[i].getbits(data_o, bit_interval);
end

The getbits task in this example also illustrates the typical use of a class method: serializing the abstract randomized data into a bit stream that can be driven onto a serial DUT input, using a ref argument and an input delay that models the bit period.[4]

The with clause and constraint inheritance

In addition to constraints declared in the class, SystemVerilog allows additional constraints to be applied at the call site using the with construct:[4]

test_message[0].randomize() with { message.DLC == 4; };

This is equivalent to writing a new constraint block inside the class. Alternatively, class inheritance can be used to create a subclass that overloads an existing constraint, fixing the value in that specialization:[4]

class CAN_Message_4 extends CAN_Message;
  constraint c1 { message.DLC == 4; }   // overload c1
endclass

Both mechanisms allow the test layer to tighten or override the default distribution without modifying the original class.[4]

Industrial case study: AMD microcode stimulus generator

The AMD/Synopsys work described in the cited design-reuse article is a concrete example of constrained-random stimulus generation applied to microprocessor microcode. It is organized as a hierarchical generator in which SystemVerilog constraints describe legal instruction combinations and the Synopsys VCS constraint solver is used to bias generation toward corner cases while reducing generation time and memory use.[3]

Generator architecture

A hierarchical constrained-random opcode generator can be organized into two layers:[3]

  1. Upper generator layer — implemented using a SystemVerilog random sequence construct. This layer uses weighted knobs to control the distribution of high-level instruction categories or opcode groups.[3]
  2. Lower opcode layer — consists of opcode classes randomized with additional constraints and weights supplied by the upper layer.[3]

Tests provide weighted values that guide the generator toward a desired instruction mix; the constraint solver applies those weights to control the distribution of generated opcode types.[3]

Single-class randomization

The simplest implementation places all opcodes and constraints in a single class. This structure is highly flexible because constraints can be written across any data members in the opcode class. In the cited implementation, the single opcode class contained approximately 100 random variables and 800 constraint equations; the class used random variables and implication constraints to ensure only legal opcodes were generated, with opcode type serving as a key field controlling the instruction kind.[3]

  • Advantages: maximum flexibility for cross-field constraints, simple object model, direct representation of all opcodes in one class.[3]
  • Disadvantages: large constraint problems, slower randomization, higher memory requirements, and potentially difficult maintenance as opcode count grows.[3]

Multi-class hierarchical randomization

To reduce the size of the randomization problem, the opcode class is split into multiple smaller classes. The opcodes are divided into a series of categories that map well to the knobs or weights used in the test interface.[5]

A base instruction class holds the data members common to all child classes, the shared methods (set, print, pack), and the constraints common to every opcode. Each child class represents an opcode category and contains only the constraints specific to that group. Within each child class, the coding structure resembles the original single-class implementation, using implication operators based on opcode type.[5]

base_instruction
 ├── arithmetic_instruction
 ├── branch_instruction
 ├── memory_instruction
 └── other opcode-category classes

Splitting constraints into smaller opcode groups reduces the number of variables and constraints seen by the solver for any one randomization call, drastically reducing memory requirements and improving performance.[3][5]

Architectural considerations and the wrapper-class pattern

The instruction generator is controlled by a set of knobs or switches that allow the test writer to generate constrained stimulus. The upper-layer random sequence is controlled by knobs only and chooses the opcode category first, allowing the correct object type to be allocated at that point and added into the sequence.[5]

If the test layer directly controls any items in the lower levels, then all decisions related to which sub-class to randomize must be made first. In that case a wrapper class is required: it constrains all of the variables controlled by the tests, is randomized first, and then the correct sub-class object is allocated and randomized in a second phase.[5]

Constraint profiling with VCS

The VCS constraint profiler analyzes generator performance in terms of runtime and memory. Runtime is reported in three views: cumulative randomize calls, per randomize call, and per partition.[5]

Profiling view Purpose
Cumulative randomize calls Shows total CPU cost accumulated across repeated calls.
Per randomize call Identifies slow individual randomization calls.
Per partition Shows performance of solver partitions within a randomization call.

The cited example showed that a randomize call in op_gen.sv at line 4308 had the largest cumulative CPU impact because it was called 7,104 times, consuming 44 seconds of CPU time, even though it executed quickly per call. Another call took 3.2 seconds individually but occurred only twice, so optimizing it would have had little overall impact. VCS can partition a randomize call into independent subproblems when random variables are unrelated, allowing them to be solved independently; the partition profile often correlates well with the individual and cumulative randomize tables.[5]

Design trade-offs

Approach Strengths Weaknesses
Single-class randomization Flexible cross-opcode constraints, simple object model Large solver problem, slower runtime, higher memory use
Multi-class hierarchical randomization Smaller solver problems, reduced memory, better performance Requires careful class partitioning and category selection logic
Wrapper-class two-phase generation Supports test-layer control of lower-level fields Adds architectural complexity and an extra randomization phase

Best practices

  • Use classes to represent transaction data and mark randomizable fields with rand.[1][2]
  • Express constraints as Boolean expressions (using inside, ==, size(), etc.) and avoid writing conflicting constraints.[2][4]
  • Provide class methods to maintain invariants (e.g. clearing the payload when RTR is set) and to serialize the abstract object to DUT pin/bit-level stimulus.[2][4]
  • Use the with clause at the call site and constraint inheritance in subclasses to tighten or override the default distribution without modifying the original class.[4]
  • Expose high-level weighted knobs to tests for distribution control and partition large opcode spaces into hierarchical subclasses to reduce solver workload.[3][5]
  • Place common fields, methods, and global constraints in a base class, and keep test-layer controls aligned with high-level opcode categories where possible.[5]
  • Use constraint profiling to prioritize optimizations by cumulative CPU cost, not only by the slowest individual call; use partition profiling to identify unrelated subproblems that can be solved independently.[5]

References

[1]: Doulos Ltd., Testbench Automation and Constraints Tutorial (SystemVerilog tutorial). Evidence excerpt on directed vs. random vs. directed-random testing, using classes to represent data structures, and functional coverage.

[2]: Doulos Ltd., Testbench Automation and Constraints Tutorial (SystemVerilog tutorial). Evidence excerpt on pseudo-random stimulus generation, constraints as Boolean expressions, the inside range operator, the role of the solver, and class methods for invariant maintenance.

[4]: Doulos Ltd., Testbench Automation and Constraints Tutorial (SystemVerilog tutorial). Evidence excerpt on writing constraints in original and derived classes, the with construct, conflicting constraints, calling randomize() from the testbench, and serializing randomized data to the DUT.

[3]: Gregory Tang and Rajat Bahl, AMD, Inc.; Alex Wakefield and Padmaraj Ramachandran, Synopsys Inc. Evidence excerpt on hierarchical constrained-random opcode generation, SystemVerilog constraints, single-class architecture, and the two-layer generator architecture.

[5]: Gregory Tang and Rajat Bahl, AMD, Inc.; Alex Wakefield and Padmaraj Ramachandran, Synopsys Inc. Evidence excerpt on multi-class randomization, architectural considerations, wrapper-class generation, and VCS constraint profiling (cumulative, per-call, and per-partition views).

CITATIONS

11 sources
11 citations
[1] Constrained-random stimulus generation is a directed-random verification technique in which a solver selects values for randomized fields under Boolean constraints, producing legal stimuli with controlled distributions, and is the central mechanism of the directed-random verification methodology. Testbench Automation and Constraints Tutorial - Doulos
[2] Most practical verification problems involve transactions best modeled by a class that wraps a rand struct, with rand fields marked accordingly; classes also allow methods and built-in pre_randomize()/post_randomize() callbacks. Testbench Automation and Constraints Tutorial - Doulos
[3] A constraint is a Boolean expression that directs the random generator to choose values satisfying the constraint; the process is called solving and the tool is called the solver. Testbench Automation and Constraints Tutorial - Doulos
[4] Constraints are class members and may be written in the original class or in derived classes; the `inside` range-membership operator and dynamic-array `size()` can be used inside constraints; conflicting constraints cause the generator to fail. Testbench Automation and Constraints Tutorial - Doulos
[5] Testbenches call `randomize()` on class objects, optionally using the `with` clause to add inline constraints; this is equivalent to writing a new constraint in the class, and class inheritance can be used to override a default constraint in a derived class. Testbench Automation and Constraints Tutorial - Doulos
[6] Class methods can maintain invariants (e.g. clearing the payload and zeroing DLC when RTR is set) and can serialize randomized data onto a serial DUT input via a `ref` argument and a delay that models the bit period. Testbench Automation and Constraints Tutorial - Doulos
[7] The AMD/Synopsys microcode generator uses a two-layer hierarchical architecture: a SystemVerilog random sequence upper layer with weighted knobs selects the opcode category, and a lower opcode layer randomizes the chosen category with category-specific constraints; SystemVerilog constraint constructs and the VCS solver are used to improve generation speed, reduce memory, and bias generation toward corner cases. Generating AMD microcode stimuli using VCS constraint solver
[8] The single-class implementation contained approximately 100 random variables and 800 constraint equations, used random variables and implication constraints to ensure only legal opcodes were generated, and traded flexibility for a large constraint-solving problem with high memory and slow runtime. Generating AMD microcode stimuli using VCS constraint solver
[9] In the multi-class architecture, a base instruction class holds common data members, shared methods, and global constraints, while each child class adds only the constraints specific to its opcode category; implication operators based on opcode type structure each child class; splitting constraints into smaller opcode groups drastically reduced memory requirements and improved performance. Generating AMD microcode stimuli using VCS constraint solver
[10] If the test layer needs to directly control items in the lower-level sub-classes, a wrapper class must constrain all test-controlled variables, be randomized first, and then allocate and randomize the correct sub-class object in a second phase. Generating AMD microcode stimuli using VCS constraint solver
[11] The VCS constraint profiler reports runtime in three views (cumulative randomize calls, per randomize call, and per partition); the randomize call at op_gen.sv line 4308 had the largest cumulative CPU impact at 44 s across 7,104 calls, while a 3.2 s call that ran only twice was a poor optimization target; VCS partitions unrelated variables into independent subproblems and the partition profile correlates with the individual and cumulative tables. Generating AMD microcode stimuli using VCS constraint solver

VERSION HISTORY

v2 · 6/9/2026 · minimax/minimax-m3 (current)
v1 · 5/24/2026 · gpt-5.5