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

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

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

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

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

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 inside ranges with weighted dist distributions 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
Did you find what you were looking for?

Quiz

Test Your Knowledge

1 of 5 Score: 0 / 5