Constraints and Constraint Solving
The previous article left a gap. An ALU’s rand bit [2:0] opcode can produce any of 8 values, but if the ALU implements 6 operations, two of those values describe operations that do not exist. Every randomize() call has roughly a one in four chance of producing input the DUT was never meant to handle. constraint blocks tell the randomization engine, the constraint solver, which values are legal, so it only ever produces those.
A basic range constraint
class alu_transaction;
rand bit [2:0] opcode;
rand bit [7:0] operand_a, operand_b;
constraint valid_opcode {
opcode inside {[0:5]}; // only opcodes 0 through 5 exist; 6 and 7 are illegal
}
endclass
Every subsequent randomize() on an object of this class produces opcode values 0 through 5 only. The solver has removed 6 and 7 from consideration, rather than generating them for you to filter out afterwards with a re-roll loop. The solver filters during generation, which becomes essential once constraints get more elaborate than one field’s range.
Relating two fields
Constraints are not limited to one field. They can express a required relationship between fields, which a per-field range cannot:
class packet_transaction;
rand bit [15:0] length;
rand bit [15:0] payload_start;
constraint valid_packet {
length inside {[64:1518]}; // Ethernet-style frame size bounds
payload_start < length; // payload can't start past the frame's own length
payload_start + 46 <= length; // must leave room for a minimum-size payload
}
endclass
Randomizing length and payload_start independently, with no relationship between them, would constantly produce a payload_start that makes no sense for the chosen length. The solver treats all three expressions as one combined problem and only produces pairs that satisfy every constraint at once.
When constraints conflict
A useful safety feature: if you write constraints that cannot be satisfied together, say opcode inside {[0:5]} alongside a contradictory opcode inside {[6:7]} elsewhere in the class, randomize() does not silently pick one or produce a value that violates the other. It fails, returning a status the testbench should check:
if (!txn.randomize()) begin
$display("ERROR: randomize() failed: constraints are unsatisfiable");
$finish;
end
This matters more than it looks. A constraint-authoring mistake (two conflicting requirements introduced while extending a class over time) is caught loudly and immediately rather than producing some arbitrary value that quietly violates one of your own stated requirements.
Soft constraints: preferences, not rules
Sometimes you want to bias randomization toward a common case without making it a hard rule. The soft keyword does that:
constraint typical_case {
soft opcode == 3'b000; // prefer ADD most of the time, but overridable
}
A soft constraint is followed whenever nothing contradicts it, and a hard constraint or an explicit override elsewhere always wins. It is useful for biasing distribution toward the common real-world case (most instructions in a real program are simple, common operations) while leaving the rest reachable.
Writing good constraints is at least as much of a skill as writing good RTL. A verification engineer’s constraint set is in effect the formal specification of “what counts as a legal input” for the DUT. Too permissive and it wastes simulation time on scenarios that can never occur; too restrictive and it lets the DUT off the hook for inputs it should handle. Getting constraints right is often cited as one of the harder parts of building a constrained-random environment.
Practice Exercise
Write constraints for a memory read/write transaction class where addr is a 10-bit field that must be word-aligned (lowest 2 bits zero), and is_write should be biased toward 0 (read) about 75 percent of the time without making writes impossible.
Solution
class mem_transaction;
rand bit [9:0] addr;
rand bit is_write;
constraint aligned_addr {
addr[1:0] == 2'b00; // word-aligned: lowest 2 bits must be zero
}
constraint write_bias {
is_write dist { 0 := 75, 1 := 25 }; // weighted distribution: 75% reads, 25% writes
}
endclass
The dist operator is the construct for weighted, non-uniform randomization, the tool for “biased toward one value most of the time but not exclusively”, distinct from soft’s “prefer this exact value unless overridden”. Both constraints apply on every randomize(), so every transaction is aligned while the read/write ratio trends toward 75/25 over many calls. The 10-bit range 0 to 1023 is already implied by the field width and needs no constraint.
constraint blocks turn raw randomization from “any bit pattern the width allows” into “only the values that are legal for this design”, expressing both single-field restrictions and relationships between fields that a solver satisfies together. A failed randomize() is a loud signal that constraints contradict each other, and soft and dist bias generation toward realistic distributions without removing the rare cases. In the next article, the last in this section, we assemble these pieces into the driver, monitor and scoreboard architecture every UVM testbench takes.