Creating Custom Types in SystemVerilog using Typedef, Enum and Struct

In this post we talking about creating our own data types in SystemVerilog using the typedef, enum and struct keywords.

In a previous post, we talked about the basic predefined data types in SystemVerilog. Whilst these are adequate for the majority of cases there are occasions when we require a custom data type in our code.

We use typedefs in SystemVerilog when we want to create a new data type. This method is very similar to the approach which is used in other programming languages such as C or C++.

In SystemVerilog, the most commonly used custom types are the enumerated types. These provide us with a technique for creating an entirely new type with custom values. We use the enum keyword to create enumerated types in SystemVerilog.

However, we can also create structured data types which we use to group a number of related variables together. We use the struct and union keywords to create these types in SystemVerilog

In the rest of this post, we will at all of these methods for creating custom types in more detail.

Systemverilog Typedef

We use the typedef keyword to create a new data type in our SystemVerlog code.

In most cases, we simply use a typedef to assign a name to a type declaration which we want to use in multiple places in our code.

This is useful as we can create quite complex data types in SystemVerilog. When we use a typedef in place of repeating a complex type declaration, we make our code simpler to understand and maintain.

The code snippet below shows the general syntax we use to create data types in SystemVerilog using typedef.

typedef <base_type> <size> <type_name>;

In this construct, we use the <base_type> field to declare what type of data we are using as a starting point for our new type.

Typically, we use one of the native SystemVerilog data types such as integer or logic in this field.

However, we can also use typedef with more complex data types such as structs, classes or enums.

The <size> field is used to declare how many bits are in our new type. This field takes the same format as we discussed in the post on data types in SystemVerilog.

We can exclude the <size> field from our declaration if it is not needed.

Finally, we use the <type_name> field to give a unique name to our new type.

Typedef Example

To better demonstrate how we use typedef in SystemVerilog let’s consider a basic example.

For this example, let’s suppose that we wanted to create an 8 bit wide logic type to use in our code.

The SystemVerilog code below shows how we would declare this new data type using a typedef.

// Create a new type called logic8
typedef logic [7:0] logic8;

It is fairly obvious from this example how we have created a new type which is equivalent to an 8 bit logic type.

After we have created a new data type, we can use it to declare variables in the same way as we would with other SystemVerilog data types.

The SystemVerilog code below shows how we would declare a variable which uses the logic8 data type we created using a typedef.

// Variable declaration using the custom type
logic8 example;

// This is equivalent to this declaration
logic [7:0] example;

Typedef With Arrays

We can also use the typedef keyword to create custom types of array in SystemVerilog.

When we want to use an array as part of a typedef, we append an extra field to the end of the declaration.

We use this extra field to declare how many elements there are in our array.

The SystemVerilog code below shows the general syntax we use to declare an array type using the typedef keyword.

typedef <base_type> <size> <type_name> <elements>;

In this construct, we use the <elements> field to declare how many elements are in our array.

As with static arrays in SystemVerilog, we can use a single number in the <elements> field to determine how many elements are in the array.

When we use this approach, SystemVerilog creates a zero indexed array as this is the most commonly used type of array. This approach is also consistent with the way that arrays in C are declared and created.

However, we can also use specify a value for the low and high indexes in our new array type. When we use this approach, the <elements> field takes the form [high_index : low_index].

Typedef Array Example

To better demonstrate how we use typedef to create custom array types, let’s consider a basic example.

For this example, suppose we wanted to create an array which consists of 4 elements of 8 bit logic vectors.

The SystemVerilog code below shows how we would use typedef to create this custom array.

// Create a custom array using typedef
typedef logic [7:0] logic8_array [4];

// Alternative array index declaration
typedef logic [7:0] logic8_array [3:0];

After we have created a new data type, we can use it to declare variables in the same way as we would with other SystemVerilog data types.

We can then access the elements of the variable in the same way we would access elements in a SystemVerilog array.

As a result of this, we can use array literals to assign data to the new data type. Alternatively, we can access individual elements of the variable using square brackets.

The code snippet below shows how we declare a variable which uses our custom array type and how we use array literals and square brackets to assign data to the variable.

// Variable declaration using the custom array type
logic8_array example;

// Using an array literal to assgin data to the variable
example = '{ 8'h01, 8'h02, 8'h03, 8'h04 };

// Using square brackets to assign data to the variable
example[0] = 8'h01;
example[1] = 8'h02;
example[2] = 8'h03;
example[3] = 8'h04;

SystemVerilog enum

We use the enum keyword in SystemVerilog to create an enumerated type.

This means that the type which we create will have a list of valid values which it can take. We explicitly list the valid values which the type can take when we create it.

In SystemVerilog, we typically use enum types to encode the states of a finite state machine.

The code snippet below shows the general syntax we use to create an enumerated type.

enum { <values> } <variable_name>;

In this construct, we use the <values> field to define the list of values which the type can take. This field takes the form of a comma separated list of strings.

After we have created an enumerated type, we can only assign it to one of the values which are listed in the <values> field.

If we attempt to assign the type to any value which isn’t explcitly listed in the <values> field, we will get either a compilation error or a warning when running a simulation. The exact behavior for this failure depends on the tools we use.

When we declare an enum type in this way, we also create a variable which we can use in our code.

We use the <variable_name> field to give a name to the variable which is created by the enum declaration.

We can see from this that by default we would need to declare the entire enumerated type for every variable which uses it.

However, if we want to use the enum type in more than one place then we can use a typedef to create a new type instead.

The code snippet below shows the general syntax we use to create an enumerated type using typedef.

typedef enum { <values> } <type_name>;

In this construct, we sue the <type_name> field to give a unique name to our new type.

After we have created a new data type in this way, we can use it to declare variables in the same way as we would with other SystemVerilog data types.

SystemVerilog enum Example

To better demonstrate how we use the enum keyword in SystemVerilog, let’s consider a basic example.

For this example, suppose that we want to create an enumerated type to use in a state machine.

The state machine has three different states which we need to include in our enum type – idle, reading, writing.

The SystemVerilog code below shows how we would create this enumerated type using enum.

enum { IDLE, READING, WRITING } example_state;

In this code snippet notice how the enumerated states are all in capital letters.

The reason for this is that SystemVerilog treats each of the enumerated values as a constant. As with most programming languages, it is common practise to use capital letters for any constants.

However, it is not mandatory for us to do this and we could just as easily use all lower case names.

Alternatively, we could use a typedef to declare the enum as a new data type. After doing this, we would then have to create a variable which takes this data type.

The code snippet below shows how would do this for our example type.

// Use typedef to create an enumerated type
typedef enum { IDLE, READING, WRITING } fsm_t;

// Create a variable which uses the enumerated type
fsm_t example_state;

Once we have created a variable which uses our enum type, we can assign data to it in the same way we would with other data types.

However, we can only assign it a value which we have explicitly listed as a part of the declaration. If we don’t follow this rule then we will either get compilation errors or warnings when we simulate it.

The code snippet below gives an example of valid and invalid data assignments to our example enum type.

// Valid data assignments
example_state = IDLE;
example_state = READING
example_state = WRITING;

// Invalid enumeration, this will cause a compilation error
example_state = UNKNOWN;

Defining enum Values

When we create an enum in SystemVerilog, we are actually creating a group of labels for an underlying int.

In fact, it is helpful to think of an enum type as a group of related constants.

By default, the actual value associated with the enumerated value will be an int. The value of the underlying int depends on the order of the values in our list.

The first entry in our enum will be assigned to the value of zero whilst the second entry will be assigned to 1.

As we can see from this, the value associated with the nth entry in the list of enum values is equal to n-1.

As an example, let’s consider the basic code snippet below.

enum { EXAMPLE0, EXAMPLE1, EXAMPLE2, EXAMPLE3 } example;

In this code example, the EXAMPLE0 constant is the first in the list whilst the EXAMPLE3 constant is the fourth value.

As a result of this, the EXAMPLE0 constant is equivalent to an int with the value of 0 whilst EXAMPLE3 is equivalent to 3.

However, we may have instances where we need to associate the constants in our enum with different values.

When we need to this we can simply specify the value which we want to associate with a constant using an equal symbol.

The code snippet below shows the general syntax we use to associate a specific value with a constant in an enumerated type.

enum { <name>=<value> } <variable_name>;

In this example, we use the <name> field to specify the name of the constant in the enum. We then use the <value> field to assign a value to this constant.

SystemVerilog enum Value Definition Example

To better demonstrate how we can define the value of an enum, let’s consider a basic example.

For this example, we will create an enumerated type for a basic FSM. We will define a total of four states in this FSM – starting, updating, running and error.

However, rather than using binary encoding for our different states we will instead use one-hot encoding.

As a result fo this, we want to associate the constants with the value of 1, 2, 4 and 8 rather than the default values.

The code snippet below shows how we would do this in SystemVerilog.

enum { STARTING=1, UPDATING=2, RUNNING=4, ERROR=8 } example;

As we can see from this example, it is relatively straight forward to associate values with the constants in an enum type.

SystemVerilog enum Methods

When we create an enumerated type in SystemVerilog, a number of methods are automatically associated with our new type.

The table below gives a list of the different methods which we can use with enum types in SystemVerilog.

SystemVerilog enum Methods
MethodDescription
prevReturns the next constant in the enumerated type
nextReturns the previous constant in the enumerated type
firstReturns the first constant in the enumerated type
lastReturns the first constant in the enumerated type
nameReturns the name of the current element in the enumerated type

Let’s look at each of these methods in more detail.

The prev and next Methods

From this list, we can see that the next and prev methods can be used to navigate through the constants in the enumerated type.

By default, the prev and next return the next or previous constant in the enumerated list.

However, it is also possible to jump forwards or backwards by more than one element in the enum.

To do this, we simply pass an argument to the method when we call it. We use this argument to tell the method how many constants to navigate through and then return.

For example, if we wanted to navigate through 3 elements of the enumerated type then we would pass 3 to to the method when we call it.

The code snippet below shows how we use the next and prev methods in practise. In addition, this code can be simulated on eda playground.

// Calling the prev and next method on an enum type
// which is given by the variable name example_enum
previous1 = example_enum.prev();
next1 = example_enum.next();

// Navigate forwards or backwards by 2 constants
previous2 = example_enum.prev(2);
next2 = example_enum.next(2);
The first and last Methods

We can use the first and last methods to return the first and last constants in our enum type. As we can see, the behavior of these methods is self explanatory.

The code snippet below shows how we use the first and last methods in practise. In addition, this code can be simulated on eda playground.

first = example_enum.first();
last = example_enum.last();
The name Method

The final method which we can use with enum types is the name method. Again, the behavior of this method is quite self explanatory.

We use this method to return the name of the enumerated constant which is currently assigned to a variable.

The code snippet below shows how we use the name method in practise. In addition, this code can be simulated on eda playground.

name = example_enum.name();

SystemVerilog Struct and Union

We use SystemVerilog structs and unions to group a number of related variables together.

This is a useful features as it allows us to create more complex data types which represents related data. For example, we could use a struct to store the three values required in an RGB type display.

The code snippet below shows the general syntax for a struct in SystemVerilog. To create a union, we would simply replace the struct keyword with the union keyword.

struct {
  // Declaration of the struct members
  <type> <name>;
  <type> <name>;
} <variable_name>;

In this construct, we use the <type> field to declare which data type the struct member will and the <name> field to give a name to the struct member.

We can include as many different members in the struct as we need to. We use a semicolon separated list to declare the full range of members which we want to include in our struct.

In addition, we can use a different data type for each of the different members in a struct or union.

Finally, we use the <variable_name> field to create a variable which we can use in the rest of our code.

As we can see from this, we would need to declare the entire struct type for every variable which uses it with this syntax.

However, we can also declare a struct using a tyepdef if we need to use the struct type in more than one place on our code.

When we do this, we use the general syntax shown in the code snippet below.

typedef struct {
  // Declaration of the struct members
  <type> <name>,
  <type> <name>
} <struct_name>;

In this construct, most of the fields are identical to the ones we previously discussed.

However, we have now introduced the <struct_name> field which we use to give a name to the new type which we create.

When we declare a struct using a tyepdef, we have to separately declare a variable which uses this type before we can use it in our code.

Union vs Struct

Although unions an structs are very similar to one another, there is an important difference in the way they work.

When we write a struct in SystemVerilog, each of the different members of the struct are allocated their own memory space.

As a result of this, we can assign values to each of the different members in the struct independently.

In contrast to this, when we write a union each of the different members of the union use a shared memory location.

This means that when we write data to one of the members in the union, all of the other members will be assigned to this value.

In fact, it is better to think of the members of a union as aliases or pointers to a memory address. We simply use the different members of the union to determine how the memory contents are interpreted.

This difference between the union and struct types in SystemVerilog is exactly the same as in the C programming language.

As structs have more flexibility than unions, we tend to use them much more frequently in our code.

SystemVerilog Struct Example

To better dmemonstrate how we use the struct type in SystemVerilog, let’s consider a simple example.

For this example, we will create a struct which we use to represent the RGB data for a pixel in a display.

To do this, we will need to create a struct which consists of three 8 bit values. We use each of these three different members to represent one of the red, green and blue fields.

The code snippet below shows how we would declare this struct type in SystemVerilog. We can also simulate this code on eda playground.

// Declaring the struct and variable in one statement
struct {
  logic [7:0] red;
  logic [7:0] green;
  logic [7:0] blue;
} rgb_example;

// Creating the struct using a tyepdef 
typedef struct {
  logic [7:0];
  logic [7:0];
  logic [7:0];
} rgb_t;

rgb_t rgb_example;

After we have declared the struct type, we can access each of the members of the struct individually.

The code snippet below shows how we would access the members of the struct. In addition, this code can be simulated on eda playground.

// Access the three members of the struct type
rgb_example.red;
rgb_example.green;
rgb_example.blue;

Exercises

When would we typically use typedefs in our SystemVerilog code? What is the advantage of using a typedef in this case

We use the typedefs in our code when we have a complex type declaration which needs to be used in several places in our code. By using a typedef in this case we can make our code easier to read and mainta

Use typedef to create a new data type which is the same as a 16 bit wide logic type. Declare a variable which uses the custom data type.

// Code to create the new data type
typedef logic [15:0] logic16;

// Declaration of variable which uses this type
logic16 example;

Write the code to declare an enum which can be used in a FSM with 4 states – idle, starting, running and stopping.

// Declaration of an enum
enum { IDLE, STARTING, RUNNING, STOPPING } example; 

// Declaration of an enum using typedef
enum { IDLE, STARTING, RUNNING, STOPPING } example_t;
example_t example;

Rewrite the code example above so that the three states of the FSM are encoded using one-hot encoding.

// Declaration of an enum
enum { IDLE=1, STARTING=2, RUNNING=4, STOPPING=8 } example; 

// Declaration of an enum using typedef
enum { IDLE=1, STARTING=2, RUNNING=4, STOPPING=8 } example_t;
example_t example;

What is the difference between a union and a struct

When we create a struct all of the variables are assigned individual memory locations. As a result of this, we can assign different values to each of the variables in a struct. In contrast, all of the variables associated with a union share a memory location. Whenever we assign data to one member of a union, all of the members are updated.

Write the code for a struct which consists of 2 int types and a logic type.

// Straight forward struct code
struct {
  int member1,
  int member2,
  logic member3
} example;

// Creating a struct using a typedef
typedef struct {
  int member1,
  int member2,
  logic member3
} example_t;

example_t example;