Drive DCRouter with fully random bits on every input and almost nothing useful happens. One header in sixteen has length 0, which is an error case; the tail's CRC is random, so 255 packets in 256 have a bad CRC; valid toggles at random in the middle of packets. The router spends the whole test reporting errors, and its normal forwarding logic, the part that carries real traffic, is barely exercised.
How do you keep the benefits of random stimulus without drowning in illegal input?
The obvious fix is to write a generator that builds legal packets by hand: pick a length from 1 to 15, compute the CRC, and so on. That works until the rules multiply: error modes, priorities tied to destinations, lengths limited in some configurations, a test that needs only port 2. Hand-written generators become tangles of special cases, and each new test needs a new generator. Constraints separate what is legal from how values are picked.
SystemVerilog lets a class declare rand fields and constraints; randomize() picks values that satisfy every constraint (fact V1). The constraints are best written and reviewed as a model of the environment: what the design's real neighbours may legally send, in each mode.
Kind of constraint | Models | DCRouter example |
|---|---|---|
Legality | What a correct source can send | In normal mode, length 1 to 15 and a good CRC |
Mode | Deliberately illegal input, switched on for error tests | In fault mode, length 0 and bad CRCs are allowed |
Relations between fields | Rules that tie fields together | At most one fault per packet |
Shape | How often each legal value appears | Boundary lengths 1 and 15 weighted up |
Inline ( | A test's focus, on top of the model | Only destination 2, high priority |
The class below generates 2,000 packets in each mode and checks every constraint on every packet, then pins two fields with an inline constraint. Two details of SystemVerilog matter here (fact V6): in a dist, := gives a weight to each value of a range while :/ shares the weight across the range; and the shape is written separately for each mode so that no weighted bucket conflicts with a hard constraint.
// Lesson 4.3: constraints as a model of the environment. A packet's fields are random, but
// only inside what a legal source can send: in normal mode every length is 1-15 and the CRC is
// good; fault mode (for error injection) also allows length 0 and makes 20% of the other
// packets carry a bad CRC. The length is shaped toward the boundaries with dist. An inline `with` pins a field for one test.
// 2,000 packets per mode are generated and every constraint is checked on every packet.
module tb;
class packet;
rand bit [1:0] dst;
rand bit prio;
rand bit [3:0] len;
rand bit bad;
bit err_mode = 0;
constraint legal { !err_mode -> (len != 0 && !bad); }
constraint one { len == 0 -> !bad; } // one fault per packet
constraint shape { // := weight per value, :/ shared by a range
if (err_mode) len dist {0 := 3, 1 := 20, [2:14] :/ 57, 15 := 20};
else len dist {1 := 20, [2:14] :/ 60, 15 := 20};
}
constraint fault { err_mode && len != 0 -> bad dist {0 := 80, 1 := 20}; }
endclass
initial begin
packet p = new();
int len_hist [16], n_bad, n_len0, n_dst2, errors;
errors = 0;
for (int mode = 0; mode < 2; mode++) begin
p.err_mode = mode;
len_hist = '{default: 0}; n_bad = 0; n_len0 = 0;
for (int k = 0; k < 2000; k++) begin
if (!p.randomize()) errors++;
len_hist[p.len]++;
n_bad += p.bad; n_len0 += (p.len == 0);
if (!mode && (p.len == 0 || p.bad)) errors++; // illegal in normal mode
if (p.len == 0 && p.bad) errors++; // two faults in one packet
end
$display("%s mode: len 0: %0d len 1: %0d len 2-14: %0d len 15: %0d bad CRC: %0d",
mode ? "fault " : "normal", n_len0, len_hist[1], 2000 - n_len0 - len_hist[1] - len_hist[15], len_hist[15], n_bad);
end
p.err_mode = 0;
n_dst2 = 0;
for (int k = 0; k < 500; k++) begin
if (!p.randomize() with { dst == 2; prio == 1; }) errors++;
n_dst2 += (p.dst == 2 && p.prio == 1);
end
$display("inline constraint dst == 2, prio == 1: %0d of 500 packets", n_dst2);
if (errors == 0 && n_dst2 == 500)
$display("PASS: every packet obeyed its mode's constraints; dist shaped the lengths; the inline constraint held 500 times");
else $display("FAIL: %0d constraint violations", errors);
$finish;
end
endmoduleIt prints (Verilator):
normal mode: len 0: 0 len 1: 401 len 2-14: 1186 len 15: 413 bad CRC: 0
fault mode: len 0: 56 len 1: 403 len 2-14: 1140 len 15: 401 bad CRC: 398
inline constraint dst == 2, prio == 1: 500 of 500 packets
PASS: every packet obeyed its mode's constraints; dist shaped the lengths; the inline constraint held 500 timesHow to read it: in normal mode no packet has length 0 or a bad CRC, and lengths 1 and 15 each appear about 20% of the time, as their weights of 20 out of 100 ask. In fault mode about 3% of packets have length 0 (56 of 2,000) and about 20% of the rest carry a bad CRC (398 of 1,944), and no packet has both faults. The inline constraint held on all 500 packets without changing the class.
Failure | What happens | Example |
|---|---|---|
Over-constrained | Legal behaviour the real environment can produce is never generated, so its bugs are never reached | Sending each input's packets in runs to one destination (a "realistic" stream) hides BUG_24, which needs back-to-back packets to different outputs |
Under-constrained | Illegal input floods the test, producing false failures or burying function under error handling | Unconstrained CRCs make almost every packet an error |
Review constraints against the specification of the neighbours, not against the design: constraints describe what may arrive, not what the design handles well.
Keep illegal input as an explicit mode: error injection is a test of its own (lesson 4.7), not leakage from loose constraints.
Cover what the constraints produce (Part 6): a constraint that silently excludes a legal case shows as a coverage hole.
Practice | Purpose |
|---|---|
Base transaction classes with legality constraints | Every test starts from a correct model of the environment |
Tests extend or add inline constraints | Focus without rewriting the generator |
Error modes as switchable constraint sets | Illegal input on purpose, never by accident |
Constraint reviews with the owners of neighbouring blocks | The model matches what neighbours really send |
A team (illustrative) constrained their bus transactions so that a new request never targeted the address of an outstanding one, because that case "never happens in our system". A later system did it constantly. The read-after-write hazard logic for overlapping addresses had never been exercised, and its bug was found in the lab. The constraint described one neighbour's habit, not the interface's rules.
len dist {1 := 20, [2:14] := 60, 15 := 20}. What fraction of packets get length 1?20 / (20 + 13·60 + 20) = 20/820, about 2.4%: := gives weight 60 to each of the 13 middle values. Writing [2:14] :/ 60 gives the intended 20%.
It is not an interface rule, and it removes back-to-back packets to different outputs, the exact case where BUG_24 (reusing the previous destination) shows.
Constraints keep random stimulus inside the legal space; write them as a model of the environment.
Separate legality, error modes, field relations, shape and per-test inline constraints.
Over-constraining hides legal cases; under-constraining floods the test; review constraints against the neighbours' specification.