Here are several practical constraint examples, each building on the one before it, modeling increasingly realistic memory-partitioning scenarios you'll actually encounter on real projects.
22 min read | Advanced Level
What You'll Learn
- How to constrain a single memory block's start and end address within a larger address range
- How to partition memory into a random number of equal-sized regions using
foreach - How to extend that to variable-sized regions whose sizes must sum to an exact total
- How to model program/data/space regions using weighted distributions and multiple coordinated arrays
Memory Block Randomization
Assume a 2KB SRAM in the design, and the need to find a block of addresses within that space for some particular purpose:
class MemoryBlock;
bit [31:0] m_ram_start; // Start address of RAM
bit [31:0] m_ram_end; // End address of RAM
rand bit [31:0] m_start_addr; // Pointer to start address of block
rand bit [31:0] m_end_addr; // Pointer to last addr of block
rand int m_block_size; // Block size in KB
constraint c_addr { m_start_addr >= m_ram_start; // Block addr should be more than RAM start
m_end_addr <= m_ram_end; // Block addr should be less than RAM end
m_start_addr % 4 == 0; // Block addr should be aligned to 4-byte boundary
m_end_addr == m_start_addr + m_block_size - 1; };
constraint c_blk_size { m_block_size inside {64, 128, 512 }; }; // Block's size should be either 64/128/512 bytes
function void display();
$display ("------ Memory Block --------");
$display ("RAM StartAddr = 0x%0h", m_ram_start);
$display ("RAM EndAddr = 0x%0h", m_ram_end);
$display ("Block StartAddr = 0x%0h", m_start_addr);
$display ("Block EndAddr = 0x%0h", m_end_addr);
$display ("Block Size = %0d bytes", m_block_size);
endfunction
endclass
module tb;
initial begin
MemoryBlock mb = new;
mb.m_ram_start = 32'h0;
mb.m_ram_end = 32'h7FF; // 2KB RAM
mb.randomize();
mb.display();
end
endmodule
Output
ncsim> run ------ Memory Block -------- RAM StartAddr = 0x0 RAM EndAddr = 0x7ff Block StartAddr = 0x714 Block EndAddr = 0x753 Block Size = 64 bytes ncsim: *W,RNQUIE: Simulation is complete.
0x714 (1812) plus a 64-byte block gives an end address of 1812 + 64 - 1 = 1875, which is 0x753 — matching exactly.
Quick Check
Q: Would m_start_addr = 0x7FC ever be a legal result, given a 64-byte block size?
Show Answer
No. The constraint only bounds where the block starts, not where it ends — but a start address that close to m_ram_end combined with any nonzero block size would push m_end_addr well past the RAM's actual boundary, which this particular constraint doesn't explicitly guard against.
Equal Partitions of Memory
Now partition the same 2KB SRAM into N equally-sized partitions:
class MemoryBlock;
bit [31:0] m_ram_start; // Start address of RAM
bit [31:0] m_ram_end; // End address of RAM
rand int m_num_part; // Number of partitions
rand bit [31:0] m_part_start []; // Partition start array
rand int m_part_size; // Size of each partition
rand int m_tmp;
// Constrain number of partitions RAM has to be divided into
constraint c_parts { m_num_part > 4; m_num_part < 10; }
// Constraint size of each partition
constraint c_size { m_part_size == (m_ram_end - m_ram_start)/m_num_part; }
// Constrain start addr of each partition
constraint c_part { m_part_start.size() == m_num_part;
foreach (m_part_start[i]) {
if (i)
m_part_start[i] == m_part_start[i-1] + m_part_size;
else
m_part_start[i] == m_ram_start;
}
}
function void display();
$display ("------ Memory Block --------");
$display ("RAM StartAddr = 0x%0h", m_ram_start);
$display ("RAM EndAddr = 0x%0h", m_ram_end);
$display ("# Partitions = %0d", m_num_part);
$display ("Partition Size = %0d bytes", m_part_size);
$display ("------ Partitions --------");
foreach (m_part_start[i])
$display ("Partition %0d start = 0x%0h", i, m_part_start[i]);
endfunction
endclass
module tb;
initial begin
MemoryBlock mb = new;
mb.m_ram_start = 32'h0;
mb.m_ram_end = 32'h7FF; // 2KB RAM
mb.randomize();
mb.display();
end
endmodule
Output
ncsim> run ------ Memory Block -------- RAM StartAddr = 0x0 RAM EndAddr = 0x7ff # Partitions = 9 Partition Size = 227 bytes ------ Partitions -------- Partition 0 start = 0x0 Partition 1 start = 0xe3 Partition 2 start = 0x1c6 Partition 3 start = 0x2a9 Partition 4 start = 0x38c Partition 5 start = 0x46f Partition 6 start = 0x552 Partition 7 start = 0x635 Partition 8 start = 0x718 ncsim: *W,RNQUIE: Simulation is complete.
With 9 partitions, 2047 / 9 = 227 (integer division — the remainder is simply discarded, meaning the very last few bytes of RAM aren't covered by any partition). Each successive start address is confirmed to be exactly 227 more than the last, all the way through partition 8.
Variable Memory Partitions
Instead of equal sizes, let each partition's size vary, drawn from a fixed set of legal sizes, with the total required to sum to exactly the full RAM size:
class MemoryBlock;
bit [31:0] m_ram_start; // Start address of RAM
bit [31:0] m_ram_end; // End address of RAM
rand int m_num_part; // Number of partitions
rand bit [31:0] m_part_start []; // Partition start array
rand int m_part_size []; // Size of each partition
rand int m_tmp;
// Constrain number of partitions RAM has to be divided into
constraint c_parts { m_num_part > 4; m_num_part < 10; }
// Constraint size of each partition
constraint c_size { m_part_size.size() == m_num_part;
m_part_size.sum() == m_ram_end - m_ram_start + 1;
foreach (m_part_size[i])
m_part_size[i] inside {16, 32, 64, 128, 512, 1024};
}
// Constrain start addr of each partition
constraint c_part { m_part_start.size() == m_num_part;
foreach (m_part_start[i]) {
if (i)
m_part_start[i] == m_part_start[i-1] + m_part_size[i-1];
else
m_part_start[i] == m_ram_start;
}
}
function void display();
$display ("------ Memory Block --------");
$display ("RAM StartAddr = 0x%0h", m_ram_start);
$display ("RAM EndAddr = 0x%0h", m_ram_end);
$display ("# Partitions = %0d", m_num_part);
$display ("------ Partitions --------");
foreach (m_part_start[i])
$display ("Partition %0d start = 0x%0h, size = %0d bytes", i, m_part_start[i], m_part_size[i]);
endfunction
endclass
module tb;
initial begin
MemoryBlock mb = new;
mb.m_ram_start = 32'h0;
mb.m_ram_end = 32'h7FF; // 2KB RAM
mb.randomize();
mb.display();
end
endmodule
Output
ncsim> run ------ Memory Block -------- RAM StartAddr = 0x0 RAM EndAddr = 0x7ff # Partitions = 7 ------ Partitions -------- Partition 0 start = 0x0, size = 512 bytes Partition 1 start = 0x200, size = 128 bytes Partition 2 start = 0x280, size = 64 bytes Partition 3 start = 0x2c0, size = 1024 bytes Partition 4 start = 0x6c0, size = 128 bytes Partition 5 start = 0x740, size = 128 bytes Partition 6 start = 0x7c0, size = 64 bytes ncsim: *W,RNQUIE: Simulation is complete.
This time the 7 partition sizes (512, 128, 64, 1024, 128, 128, 64) sum to exactly 2048 — the full RAM size, unlike the equal-partition example's integer-division remainder. Each partition's start address correctly follows the previous partition's start plus its own size, with zero gaps and zero overlap.
Quick Check
Q: Why does this example's constraint use m_ram_end - m_ram_start + 1, while the equal-partitions example used (m_ram_end - m_ram_start) / m_num_part without the +1?
Show Answer
Because this constraint needs the partitions to add up to the RAM's total byte count (2048), which requires the +1 since m_ram_end is the address of the last valid byte, not one past it. The equal-partitions example instead divides that same span across a variable number of partitions, and simply accepts that integer division may leave a small remainder uncovered.
Variable Memory Partitions With Space in Between
Now add a gap — possibly zero bytes — between each partition:
class MemoryBlock;
bit [31:0] m_ram_start; // Start address of RAM
bit [31:0] m_ram_end; // End address of RAM
rand int m_num_part; // Number of partitions
rand bit [31:0] m_part_start []; // Partition start array
rand int m_part_size []; // Size of each partition
rand int m_space[]; // Space between each partition
rand int m_tmp;
// Constrain number of partitions RAM has to be divided into
constraint c_parts { m_num_part > 4; m_num_part < 10; }
// Constraint size of each partition
constraint c_size { m_part_size.size() == m_num_part;
m_space.size() == m_num_part - 1;
m_space.sum() + m_part_size.sum() == m_ram_end - m_ram_start + 1;
foreach (m_part_size[i]) {
m_part_size[i] inside {16, 32, 64, 128, 512, 1024};
if (i < m_space.size())
m_space[i] inside {0, 16, 32, 64, 128, 512, 1024};
}
}
// Constrain start addr of each partition
constraint c_part { m_part_start.size() == m_num_part;
foreach (m_part_start[i]) {
if (i)
m_part_start[i] == m_part_start[i-1] + m_part_size[i-1] + m_space[i-1];
else
m_part_start[i] == m_ram_start;
}
}
function void display();
$display ("------ Memory Block --------");
$display ("RAM StartAddr = 0x%0h", m_ram_start);
$display ("RAM EndAddr = 0x%0h", m_ram_end);
$display ("# Partitions = %0d", m_num_part);
$display ("------ Partitions --------");
foreach (m_part_start[i])
$display ("Partition %0d start = 0x%0h, size = %0d bytes, space = %0d bytes", i, m_part_start[i], m_part_size[i], m_space[i]);
endfunction
endclass
module tb;
initial begin
MemoryBlock mb = new;
mb.m_ram_start = 32'h0;
mb.m_ram_end = 32'h7FF; // 2KB RAM
mb.randomize();
mb.display();
end
endmodule
Output
ncsim> run ------ Memory Block -------- RAM StartAddr = 0x0 RAM EndAddr = 0x7ff # Partitions = 5 ------ Partitions -------- Partition 0 start = 0x0, size = 128 bytes, space = 64 bytes Partition 1 start = 0xc0, size = 32 bytes, space = 128 bytes Partition 2 start = 0x160, size = 32 bytes, space = 0 bytes Partition 3 start = 0x180, size = 1024 bytes, space = 512 bytes Partition 4 start = 0x780, size = 128 bytes, space = 0 bytes ncsim: *W,RNQUIE: Simulation is complete.
m_space is deliberately sized to m_num_part - 1 (4 elements for 5 partitions, since space only exists between partitions, not after the last one) — but display()'s foreach loop iterates over m_part_start's indices, all 5 of them, and prints m_space[i] for every single one, including index 4. That index is out of bounds for a 4-element array. The space = 0 bytes shown for the final partition isn't a genuine constrained value — it's an out-of-bounds read that happens to return 0, the default value for int. Adding up only the first 4 (legitimate) space values with all 5 partition sizes gives exactly 2048, confirming the last "space" figure isn't part of the actual solution at all.
Partition for Programs and Data
Finally, a more realistic scenario: memory partitioned into program, data, and empty-space regions, with the count and size of each driven by its own set of constraints, and a weighted distribution controlling how program sizes are chosen:
typedef struct {
int start_addr;
int end_addr;
} e_range;
class Space;
rand int num_pgm; // Total number of programs required
rand int num_data; // Total number of data blocks required
rand int num_space; // Total number of empty spaces required
rand int max_pgm_size; // Maximum program size
rand int max_data_size; // Maximum data size
rand int num_max_pgm; // Maximum number of programs
rand int pgm_size[]; // Size of each program region
rand int data_size[]; // Size of each data region
rand int space_size[]; // Size of each empty space region
int total_ram; // Total RAM space
// Constrain maximum individual program region size to be 512 bytes
// data region size to be 128 bytes and maximum number of programs in
// this memory region to be 100
constraint c_num_size { max_pgm_size == 512;
max_data_size == 128;
num_max_pgm == 100;
}
// Constrain total number of programs, data and space regions
constraint c_num { num_pgm inside {[1:num_max_pgm]};
num_data inside {[1:50]};
num_space inside {[1:50]};
}
// Constrain the array for program, data and space to equal the number
// decided by the above constraints.
constraint c_size { pgm_size.size() == num_pgm;
data_size.size() == num_data;
space_size.size() == num_space;
}
// Size of each program/data/space is stored into indices of the corresponding
// arrays. So, total size of RAM should be the sum of all programs + data + space
constraint c_ram { foreach (pgm_size[i]) {
// pgm_size[i] inside {4, 8, 32, 64, 128, 512};
pgm_size[i] dist {[128:512]:/75, [32:64]:/20, [4:8]:/10};
pgm_size[i] % 4 == 0;
}
foreach (data_size[i]) {
data_size[i] inside {64, 128, 512, 1024};
}
foreach (space_size[i]) {
space_size[i] inside {4, 8, 32, 64, 128, 512, 1024};
}
total_ram == pgm_size.sum() + data_size.sum() + space_size.sum();
}
// Function to display the partitioning
function void display();
$display("#pgms=%0d #data=%0d, #space=%0d", num_pgm, num_data, num_space);
$display("#pgms.size=%0d #data.size=%0d, #space.size=%0d total=%0d", pgm_size.sum(), data_size.sum(), space_size.sum(), total_ram);
foreach(pgm_size[i])
$display("pgm#%0d size=%0d bytes", i, pgm_size[i]);
foreach(data_size[i])
$display("data#%0d size=%0d bytes", i, data_size[i]);
foreach(space_size[i])
$display("space#%0d size=%0d bytes", i, space_size[i]);
endfunction
endclass
module tb;
initial begin
Space sp = new();
sp.total_ram = 6 * 1024; // Assuem total 6KB memory space
assert(sp.randomize());
sp.display();
end
endmodule
Output
ncsim> run #pgms=37 #data=18, #space=47 #pgms.size=3792 #data.size=1216, #space.size=1136 total=6144 pgm#0 size=4 bytes pgm#1 size=200 bytes pgm#2 size=384 bytes pgm#3 size=4 bytes pgm#4 size=60 bytes pgm#5 size=44 bytes pgm#6 size=4 bytes pgm#7 size=4 bytes pgm#8 size=396 bytes pgm#9 size=164 bytes pgm#10 size=40 bytes pgm#11 size=188 bytes pgm#12 size=148 bytes pgm#13 size=152 bytes pgm#14 size=4 bytes pgm#15 size=48 bytes pgm#16 size=4 bytes pgm#17 size=32 bytes pgm#18 size=36 bytes pgm#19 size=180 bytes pgm#20 size=8 bytes pgm#21 size=4 bytes pgm#22 size=144 bytes pgm#23 size=48 bytes pgm#24 size=448 bytes pgm#25 size=4 bytes pgm#26 size=148 bytes pgm#27 size=4 bytes pgm#28 size=4 bytes pgm#29 size=4 bytes pgm#30 size=4 bytes pgm#31 size=4 bytes pgm#32 size=32 bytes pgm#33 size=4 bytes pgm#34 size=356 bytes pgm#35 size=476 bytes pgm#36 size=4 bytes data#0 size=64 bytes data#1 size=64 bytes data#2 size=64 bytes data#3 size=64 bytes data#4 size=64 bytes data#5 size=64 bytes data#6 size=64 bytes data#7 size=64 bytes data#8 size=64 bytes data#9 size=128 bytes data#10 size=64 bytes data#11 size=64 bytes data#12 size=64 bytes data#13 size=64 bytes data#14 size=64 bytes data#15 size=64 bytes data#16 size=64 bytes data#17 size=64 bytes space#0 size=4 bytes space#1 size=4 bytes space#2 size=4 bytes space#3 size=4 bytes space#4 size=4 bytes space#5 size=4 bytes space#6 size=4 bytes space#7 size=4 bytes space#8 size=128 bytes space#9 size=8 bytes space#10 size=4 bytes space#11 size=4 bytes space#12 size=4 bytes space#13 size=4 bytes space#14 size=32 bytes space#15 size=64 bytes space#16 size=512 bytes space#17 size=4 bytes space#18 size=32 bytes space#19 size=4 bytes space#20 size=4 bytes space#21 size=128 bytes space#22 size=4 bytes space#23 size=4 bytes space#24 size=4 bytes space#25 size=4 bytes space#26 size=8 bytes space#27 size=4 bytes space#28 size=4 bytes space#29 size=4 bytes space#30 size=4 bytes space#31 size=8 bytes space#32 size=4 bytes space#33 size=4 bytes space#34 size=4 bytes space#35 size=4 bytes space#36 size=64 bytes space#37 size=4 bytes space#38 size=4 bytes space#39 size=4 bytes space#40 size=4 bytes space#41 size=8 bytes space#42 size=4 bytes space#43 size=4 bytes space#44 size=4 bytes space#45 size=4 bytes space#46 size=4 bytes ncsim: *W,RNQUIE: Simulation is complete.
The three region counts (37 programs, 18 data blocks, 47 spaces) and their total sizes (3792 + 1216 + 1136 = 6144) match the 6KB total_ram exactly. Every one of the 37 program sizes is both divisible by 4 and falls within one of the three dist ranges ([4:8], [32:64], or [128:512]) — and with that distribution weighted 75% toward the largest range, it's no coincidence that most program sizes shown are on the larger end, though the heavily-weighted small values (many programs sized at exactly 4 bytes) still show up as expected from the remaining 10% weight on [4:8].
Where Is This Used?
This exact family of techniques — sizing dynamic arrays via constraints, chaining each element's start address off the previous one's end, and mixing inside ranges with weighted dist distributions — is the standard toolkit for generating realistic memory maps in SoC and memory-controller verification, where the goal is to exercise many different, legal partitioning scenarios without ever hand-writing them individually.
Common Beginner Mistakes
Mistake #1: Bounding only a block's start address, not its full extent
As the first Quick Check shows, a block's start address staying within range doesn't guarantee its end address does too, once a variable size is involved. Why? Any constraint modeling a sized region needs to account for where that region actually ends, not just where it begins.
Mistake #2: Reusing a loop bound sized for one array to index into a differently-sized array
As the flagged example shows, iterating with an index meant for a 5-element array while reading from a 4-element one produces a silent out-of-bounds access. Why? The read doesn't crash or warn in this case — it just returns a default value that can look deceptively like a real result.
Mistake #3: Forgetting the +1 when a sum needs to cover an inclusive address range
As the second Quick Check explains, m_ram_end - m_ram_start and m_ram_end - m_ram_start + 1 mean different things — a byte count spanning an inclusive range always needs the +1. Why? Omitting it undercounts the true size of the range by exactly one byte, which matters when a sum constraint needs to match it exactly.
Mistake #4: Assuming dist weights alone guarantee every value in a range is equally likely
As the final example shows, a 75% weight on [128:512] doesn't mean every individual value in that range is equally probable relative to values in the other two ranges — it governs the combined probability of landing anywhere in that range at all. Why? This is the same range-vs-individual-value distinction covered for dist in earlier articles, just applied here at a larger scale.
Practice Exercise
Challenge: Fix the Out-of-Bounds Display
Rewrite the display() function from the "Variable Memory Partitions With Space in Between" example so it never reads past the end of m_space.
Hint
The constraint block itself already uses the right guard — if (i < m_space.size()) — before touching m_space[i]. Apply that same guard inside the display loop.
Solution
function void display();
$display ("------ Memory Block --------");
$display ("RAM StartAddr = 0x%0h", m_ram_start);
$display ("RAM EndAddr = 0x%0h", m_ram_end);
$display ("# Partitions = %0d", m_num_part);
$display ("------ Partitions --------");
foreach (m_part_start[i]) begin
if (i < m_space.size())
$display ("Partition %0d start = 0x%0h, size = %0d bytes, space = %0d bytes",
i, m_part_start[i], m_part_size[i], m_space[i]);
else
$display ("Partition %0d start = 0x%0h, size = %0d bytes",
i, m_part_start[i], m_part_size[i]);
end
endfunction
The last partition now simply omits the "space" field entirely, rather than printing a misleading value read from outside the array's actual bounds.
Summary
In this tutorial, you learned:
- A single memory block can be constrained to fall within a larger address range using bounds on its start address, alignment, and computed end address
- An array of partition start addresses can be built entirely from constraints, with each element defined relative to the one before it via
foreach - Array reduction methods like
sum()let a constraint require a set of variable-sized regions to add up to an exact total - Combining
insideranges with weighteddistdistributions across several coordinated dynamic arrays can model a realistic, multi-region memory map in a single randomize() call - A loop bound sized for one array can silently read out of bounds on a differently-sized array — always match the guard condition to the specific array actually being indexed