Randomness Unleashed: Basic Randomization in Verilator

In a previous blog post, If-Else Constraint Support, we dove into the importance of Constrained Random Testing (CRT) for verification, and we emphasized just how crucial randomization is to SystemVerilog. Randomization can be divided into two categories: basic randomization (or unconstrained randomization) and constrained randomization. These two essential features of SystemVerilog, however, aren’t fully supported by Verilator… yet.

But things are changing! Since May, Verilator has seen a lot of active development in this area. PlanV, as one of the enthusiastic contributors to Verilato’s development, has jumped into the fray and made significant contributions. Today, we’ll start with basic randomization and dive into how PlanV has contributed to supporting basic randomization for aggregate data types in Verilator.

Meet the Family: Aggregate Data Types in SystemVerilog

Before we jump into the technical details, let’s first explore what aggregate data types are and why they matter. Straight from IEEE Std 1800™-2023, aggregate data types in SystemVerilog include structs, unions, arrays, and queues. While I won’t get into the nitty-gritty of each, I’ve prepared a diagram (Fig. 1) to help visualize how these types relate to one another.

Fig.1 Aggregate Date Types in SystemVerilog

That’s quite a variety of types, but once you understand their relationships, it all starts to make sense! Let’s break things down by starting with the most fundamental distinction:

Packed vs. Unpacked

The first key difference to understand is between packed and unpacked data types:

  • Packed: All elements are stored tightly together in memory, with no gaps. Think of it like efficiently packing a suitcase—everything fits snugly.
  • Unpacked: Here, elements are stored more loosely, allowing for flexibility in memory allocation.

Unpacked Arrays

Zooming in on unpacked arrays, we can further divide them into:

  • Fixed-size unpacked arrays: These have a set size, determined at compile time.
  • Variable-size unpacked arrays: These can grow or shrink dynamically at runtime.

Among variable-size unpacked arrays, we have three main types:

  • Dynamic arrays: Allocate memory sequentially, element by element.
  • Queues: Similar to dynamic arrays, but with a push-back mechanism, where elements are added at the end.
  • Associative arrays: Use keys for memory allocation, making them more flexible, but less predictable in terms of memory layout.

Structs vs. Unions

Lastly, let’s not forget about structs and unions. These two aggregate types are often confused, but they work quite differently:

  • Structs: Store each member side by side in memory, with each member having its own space.
  • Unions: All members share the same memory location, meaning only one member can be accessed at a time. Assigning to one member overwrites the others.

Breaking It Down: How PlanV Tackled Basic Randomization for Aggregate Data Types in Verilator

Now, let’s dive into how PlanV helped bring basic randomization support for aggregate data types in Verilator. To understand how it all works, let’s first look at what Verilator currently does.

It’s important to introduce two key concepts in Verilator’s workflow: the verilate phase and the simulate phase. The verilate phase is when Verilator translates RTL code into an AST tree, optimizes it (in verilator/src folder), and generates C++ code. In the simulate phase, this generated C++ code, with support from Verilator’s built-in functions (in verilator/include folder), runs to produce simulation results.

As of Verilator v.5.029, for basic randomization, Verilator uses an internal random number generator (RNG) to create a large block of random data. This data is then adjusted based on the width of each variable and assigned accordingly. This whole process happens during the verilate phase, where the Abstract Syntax Tree (AST) is optimized.

When it comes to constrained randomization, Verilator handles it a bit differently. During the verilate phase, it converts the constraint expressions into SMT-LIB2 format. After that, in the simulate phase, an external solver like Z3 Solver is called to process these constraints and provide a solution.

PlanV’s Challenge: Supporting Aggregate Data Types

When PlanV started working on this, we found that Verilator, at the time, only supported basic data types like logic , bit , and enumerated types. Aggregate data types like arrays, structs, and unions lacked full randomization support, limiting the tool’s capacity to handle complex data structures common in verification environments. We took on the challenge of designing a solution capable of managing all aggregate data types, allowing Verilator to generate realistic and diverse test scenarios.

Here’s the approach we took (see Fig. 2 for reference):

Fig.2 Basic Randomization Support for all kinds of data types in Verilator

  1. Breaking Down Data Types:
    The first step is to check the type of data. If it’s an aggregate type—such as a struct, union, or array—we break it down into its basic components. The idea here is to simplify complex data types into basic ones, which can then follow the standard randomization process (where the RNG is applied).
  2. Handling Structs and Unions:
    For structs and unions, Verilator treats their data structures similarly. When we deal with unpacked structs and unions, we break them down by picking individual elements. For packed structs, we not only pick elements but also add an offset to keep track of their width (see Fig. 3 for reference).
    One important note: according to IEEE Std 1800™-2023 section 18.4, packed unions shall not be declared as rand or randc . If an packed union is detected, Verilator will throw an error (see Fig. 4 for reference).

Fig.3 Struct Handling

Fig.4 Union Handling

  1. Array Handling with Foreach Loops:
    Arrays bring their own challenges. For packed arrays, according to the IEEE Std 1800™-2023 section 7.4, they can often be treated as a long vector, so we handle them like any basic data type (see Fig. 5 for reference). However, for unpacked arrays, Verilator defines different types, making manual breakdown inefficient. To address this, we use a function called createForeachLoop , which generates a foreach loop for all unpacked array types.

    In createForeachLoop function, shown in Fig.6, we set up the loop index, locate the basic elements of the array, and store the process in a statement pointer. This method keeps the code short, clean, and reusable, while also preventing the creation of long and redundant AST trees, especially when dealing with large, multi-dimensional arrays.

Fig.5 Array Handling

Fig.6 CreateForeachLoop

With these implementations, we successfully added basic randomization support for all aggregate data types. All relevant pull requests (PR #5396, PR #5415, PR #5515) have been merged into the Verilator repository, so if you’re using the latest version of Verilator, good news— you can try it out!

The Underlying Issues We Faced (And How We Crushed Them)

Of course, no development journey is complete without its fair share of hiccups. Let’s talk about some of the issues we ran into during the development of basic randomization support—and how we tackled them. These issues were all uncovered through our CI system. (By the way, if you haven’t read about our CI system yet, check it out here!)

First up: the foreach issue. We noticed that when handling variable-size unpacked arrays, Verilator couldn’t determine the correct size, and as a result, it couldn’t iterate over the correct boundaries. Curious how we fixed that? Check out PR #5529 and PR #5530.

Next, struct array assignments issue. During testing, we found that when assigning values to an array where each element is a struct, the fields were getting overwritten. This was because Verilator’s assignment handling introduced a temporary variable, but didn’t account for struct arrays. Wonder how we fixed that? Check out PR #5537.

Lastly, the multi-range index issue. When dealing with unpacked arrays that aren’t indexed by a single value (e.g., [3:1] or [4:9] ), Verilator was having trouble handling the boundaries and assigning values in the correct order. Curious about the fix? Head over to PR #5547.

Thanks to our CI system, we caught many of these issues early on—some that we hadn’t anticipated during initial development. And as of now, we’ve solved all the known or reported issues. If you run into any problems while using Verilator’s basic randomization, don’t hesitate to join our community and submit your issues. We’ll work quickly to resolve them and make this functionality even better.

PlanV isn’t stopping here on the road to developing UVM for Verilator. Next time, we’ll dive into constrained randomization for all kinds of arrays. Follow us for more updates!