Digital blocks typically communicate with each other using bus protocols, a few examples of which includes AMBA AXI, WishBone, OCP, etc. Bus masters that send out data adhering to a certain protocol provide control signals that tell the slave when the packet is valid, and whether it is a read or write, and how many bytes of data is sent. The master also sends out an address followed by the data to be stored at that address.
Let's see a quick example where the testbench acts as the master and constrains the bus packet class object with valid data.
// Burst [ 0 -> 1 byte, 1 -> 2 bytes, 2 -> 3 bytes, 3 -> 4 bytes]
// Length -> max 8 transactions per burst
// Protocol expects to send only first addr, and slave should calculate all
// other addresses from burst and length properties
class BusTransaction;
rand int m_addr;
rand bit [31:0] m_data;
rand bit [1:0] m_burst; // Size of a single transaction in bytes (4 bytes max)
rand bit [2:0] m_length; // Total number of transactions
constraint c_addr { m_addr % 4 == 0; } // Always aligned to 4-byte boundary
function void display(int idx = 0);
$display ("------ Transaction %0d------", idx);
$display (" Addr = 0x%0h", m_addr);
$display (" Data = 0x%0h", m_data);
$display (" Burst = %0d bytes/xfr", m_burst + 1);
$display (" Length = %0d", m_length + 1);
endfunction
endclass
module tb;
int slave_start;
int slave_end;
BusTransaction bt;
// Assume we are targeting a slave with addr range 0x200 to 0x800
initial begin
slave_start = 32'h200;
slave_end = 32'h800;
bt = new;
bt.randomize() with { m_addr >= slave_start;
m_addr < slave_end;
(m_burst + 1) * (m_length + 1) + m_addr < slave_end;
};
bt.display();
end
endmodule
ncsim> run ------ Transaction 0------ Addr = 0x6e0 Data = 0xbbe5ea58 Burst = 4 bytes/xfr Length = 5 ncsim: *W,RNQUIE: Simulation is complete.
Consider the following practical examples typically encountered during actual projects.
Memory block randomization
Assume we have a 2KB SRAM in the design intended to store some data. Let's say that we need to find a block of addresses within the 2KB RAM space that can be used 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_start_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
In the example above, we have assumed the RAM to start from 0x0 and end at 0x7FF. The constraint example aims to allocate a block of memory space between this range with a size that is randomly chosen from 64 or 128 or 512 bytes. The start address of the block is randomized to be 0x714 and hence the end addr is 0x753.
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.
Equal partitions of memory
In this example, we'll try to partition the 2KB SRAM into N partitions with each parititon having equal size.

SystemVerilog randomization also works on array data structures like static arrays, dynamic arrays and queues. The variable has to be declared with type rand
or randc
to enable randomization of the variable.
Static Arrays
Randomization of static arrays are straight-forward and can be done similar to any other type of SystemVerilog variable.
class Packet;
rand bit [3:0] s_array [7]; // Declare a static array with "rand"
endclass
module tb;
Packet pkt;
// Create a new packet, randomize it and display contents
initial begin
pkt = new();
pkt.randomize();
$display("queue = %p", pkt.s_array);
end
endmodule
ncsim> run
queue = '{'hf, 'hf, 'h2, 'h9, 'he, 'h4, 'ha}
ncsim: *W,RNQUIE: Simulation is complete.
Dynamic Arrays
Dynamic arrays are arrays where the size is not pre-determined during array declaration. These arrays can have variable size as new members can be added to the array at any time.
Consider the example below where we declare a dynamic array as indicated by the empty square brackets []
of type rand
. A constraint is defined to limit the size of the dynamic array to be somewhere in between 5 and 8. Another constraint is defined to assign each element in the array with the value of its index.
class Packet;
rand bit [3:0] d_array []; // Declare a dynamic array with "rand"
// Constrain size of dynamic array between 5 and 10
constraint c_array { d_array.size() > 5; d_array.size() < 10; }
// Constrain value at each index to be equal to the index itself
constraint c_val { foreach (d_array[i])
d_array[i] == i;
}
// Utility function to display dynamic array contents
function void display();
foreach (d_array[i])
$display ("d_array[%0d] = 0x%0h", i, d_array[i]);
endfunction
endclass
module tb;
Packet pkt;
// Create a new packet, randomize it and display contents
initial begin
pkt = new();
pkt.randomize();
pkt.display();
end
endmodule
Randomization yields an empty array if the size is not constrainted -> applicable for dynamic arrays and queues
Note that the array size was randomized to 9 (from constraint c_array), and the element at each index has a value of the index itself (from constraint c_val.
ncsim> run d_array[0] = 0x0 d_array[1] = 0x1 d_array[2] = 0x2 d_array[3] = 0x3 d_array[4] = 0x4 d_array[5] = 0x5 d_array[6] = 0x6 d_array[7] = 0x7 d_array[8] = 0x8 ncsim: *W,RNQUIE: Simulation is complete.
Queue randomization
class Packet;
rand bit [3:0] queue [$]; // Declare a queue with "rand"
// Constrain size of queue between 5 and 10
constraint c_array { queue.size() == 4; }
endclass
module tb;
Packet pkt;
// Create a new packet, randomize it and display contents
initial begin
pkt = new();
pkt.randomize();
// Tip : Use %p to display arrays
$display("queue = %p", pkt.queue);
end
endmodule
ncsim> run
queue = '{'hf, 'hf, 'h2, 'h9}
ncsim: *W,RNQUIE: Simulation is complete.
What is a class handle ?
A class variable such as pkt below is only a name by which that object is known. It can hold the handle to an object of class Packet, but until assigned with something it is always null
. At this point, the class object does not exist yet.
Class Handle Example
// Create a new class with a single member called
// count that stores integer values
class Packet;
int count;
endclass
module tb;
// Create a "handle" for the class Packet that can point to an
// object of the class type Packet
// Note: This "handle" now points to NULL
Packet pkt;
initial begin
if (pkt == null)
$display ("Packet handle 'pkt' is null");
// Display the class member using the "handle"
// Expect a run-time error because pkt is not an object
// yet, and is still pointing to NULL. So pkt is not
// aware that it should hold a member
$display ("count = %0d", pkt.count);
end
endmodule
ncsim> run Packet handle 'pkt' is null count = ncsim: *E,TRNULLID: NULL pointer dereference. File: ./testbench.sv, line = 18, pos = 33 Scope: tb Time: 0 FS + 0 ./testbench.sv:18 $display ("count = %0d", pkt.count); ncsim> exit
What is a class object ?
An instance of that class is created only when it's new()
function is invoked. To reference that particular object again, we need to assign it's handle to a variable of type Packet.
Class Object Example
// Create a new class with a single member called
// count that stores integer values
class Packet;
int count;
endclass
module tb;
// Create a "handle" for the class Packet that can point to an
// object of the class type Packet
// Note: This "handle" now points to NULL
Packet pkt;
initial begin
if (pkt == null)
$display ("Packet handle 'pkt' is null");
// Call the new() function of this class
pkt = new();
if (pkt == null)
$display ("What's wrong, pkt is still null ?");
else
$display ("Packet handle 'pkt' is now pointing to an object, and not NULL");
// Display the class member using the "handle"
$display ("count = %0d", pkt.count);
end
endmodule
ncsim> run
Packet handle 'pkt' is null
Packet handle 'pkt' is now pointing to an object, and not NULL
count = 0
ncsim: *W,RNQUIE: Simulation is complete.
What happens when both handles point to same object ?
If we assign pkt to a new variable called pkt2, the new variable will also point to the contents in pkt.
// Create a new class with a single member called
// count that stores integer values
class Packet;
int count;
endclass
module tb;
// Create two "handles" for the class Packet
// Note: These "handles" now point to NULL
Packet pkt, pkt2;
initial begin
// Call the new() function of this class and
// assign the member some value
pkt = new();
pkt.count = 16'habcd;
// Display the class member using the "pkt" handle
$display ("[pkt] count = 0x%0h", pkt.count);
// Make pkt2 handle to point to pkt and print member variable
pkt2 = pkt;
$display ("[pkt2] count = 0x%0h", pkt2.count);
end
endmodule
ncsim> run [pkt] count = 0xabcd [pkt2] count = 0xabcd ncsim: *W,RNQUIE: Simulation is complete.
Now we have two handles, pkt and pkt2 pointing to the same instance of the class type Packet. This is because we did not create a new instance for pkt2 and instead only assigned a handle to the instance pointed to by pkt.
wait fork
allows the main process to wait until all forked processes are over. This is useful in cases where the main process has to spawn multiple threads, and perform some function before waiting for all threads to finish.
Example
We'll use the same example seen in the previous article where 3 threads are kicked off in parallel and the main process waits for one of them to finish. After the main thread resumes, let's wait until all forked processes are done.