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]
- 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]
- 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
withclause 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).