SystemVerilog for Design Edition 2 Chapter 4 SystemVerilog User-Defined and Enumerated Types
SystemVerilog for Design Edition 2 Chapter 4
SystemVerilog User-Defined and Enumerated Types
SystemVerilog makes a significant extension to the Verilog language by allowing users to define new net and variable types. User-defined types allow modeling complex designs at a more abstract level that is still accurate and synthesizable. Using System-Verilog’s user-defined types, more design functionality can be modeled in fewer lines of code, with the added advantage of making the code more self-documenting and easier to read. The enhancements presented in this chapter include:
• Using typedef to create user-defined types
• Using enum to create enumerated types
• Working with enumerated values
4.1 User-defined types
The Verilog language does not provide a mechanism for the user to extend the language net and variable types. While the existing Verilog types are useful for RTL and gate-level modeling, they do not provide C-like variable types that could be used at higher levels of abstraction. SystemVerilog adds a number of new types for modeling at the system and architectural level. In addition, SystemVerilog adds the ability for the user to define new net and variable types.
typedef defines a user-defined type
SystemVerilog user-defined types are created using the typedef keyword, as in C. User-defined types allow new type definitions to be created from existing types. Once a new type has been defined, variables of the new type can be declared. For example:
typedef int unsigned uint;
...
uint a, b; // two variables of type uint
4.1.1 Local typedef definitions
using typedef locally
User-defined types can be defined locally, in a package, or externally, in the compilation-unit scope. When a user-defined type will only be used within a specific part of the design, the typedef definition can be made within the module or interface representing that portion of the design. Interfaces are presented in Chapter 10. In the code snippet that follows, a user-defined type called nibble is declared, which is used for variable declarations within a module called alu. Since the nibble type is defined locally, only the alu module can see the definition. Other modules or interfaces that make up the overall design are not affected by the local definition, and can use the same nibble identifier for other purposes without being affected by the local typedef definition in module alu.
module alu (...);
typedef logic [3:0] nibble;
nibble opA, opB; // variables of the
// nibble type
nibble [7:0] data; // a 32-bit vector made
// from 8 nibble types
...
endmodule
4.1.2 Shared typedef definitions
typedef definitions in packages
When a user-defined type is to be used in many different models, the typedef definition can be declared in a package. These definitions can then be referenced directly, or imported into each module, interface or program block that uses the user-defined types. The use of packages is discussed in Chapter 2, section 2.1 on page 8.
typedef definitions in $unit
A typedef definition can also be declared externally, in the compilation-unit scope. External declarations are made by placing the
typedef statement outside of any module, interface or program block, as was discussed in Chapter 2, section 2.2 on page 14.
Example 4-1 illustrates the use of a package typedef definition to create a user-defined type called dtype_t, that will be used
throughout the design. The typedef definition is within an ‘ifdef conditional compilation directive, that defines dtype_t to
be either the 2-state bit type or the 4-state logic type. Using conditional compilation, all design blocks that use the dtype_t userdefined type can be quickly modified to model either 2-state or 4-state logic.
Example 4-1: Directly referencing typedef definitions from a package
package chip_types;
`ifdef TWO_STATE
typedef bit dtype_t;
`else
typedef logic dtype_t;
`endif
endpackage
module counter
(output chip_types::dtype_t [15:0] count,
input chip_types::dtype_t clock, resetN);
always @(posedge clock, negedge resetN)
if (!resetN) count <= 0;
else count <= count + 1;
endmodule
importing package definitions into $unit
It is also possible to import package definitions into the $unit
compilation-unit space. This can be useful when many ports of a module are of user-defined types, and it becomes tedious to directly reference the package name for each port declaration. Example 4-2 illustrates importing a package definition into the $unit space
, for use as a module port type.
Example 4-2: Importing package typedef definitions into $unit
package chip_types;
`ifdef TWO_STATE
typedef bit dtype_t;
`else
typedef logic dtype_t;
`endif
endpackage
import chip_types::dtype_t; // import definition into $unit
module counter
(output dtype_t [15:0] count,
input dtype_t clock, resetN);
always @(posedge clock, negedge resetN)
if (!resetN) count <= 0;
else count <= count + 1;
endmodule
If the package contains many typedefs, instead of importing specific package items into the $unit
compilation-unit space, the package can be wildcard imported into $unit
.
import chip_types::*; // wildcard import
4.1.3 Naming convention for user-defined types
A user-defined type can be any legal name in the Verilog language. In large designs, and when using external compilation-unit scope declarations, the source code where a new user-defined type is defined and the source code where a user-defined type is used could be separated by many lines of code, or in separate files. This separation of the typedef definition and the usage of the new types can make it difficult to read and maintain the code for large designs. When a name is used in the source code, it might not be obvious that the name is actually a user-defined type.
To make source code easier to read and maintain, a common naming convention is to end all user-defined types with the characters “_t”. This naming convention is used in example 4-1, above, as well as in many subsequent examples in this book.
4.2 Enumerated types
Enumerated types provide a means to declare an abstract variable that can have a specific list of valid values. Each value is identified with a user-defined name, or label. In the following example, variable RGB can have the values of red, green and blue:
enum {red,green,blue} RGB;
Verilog style for labeling values
Verilog uses constants in place of enumerated types
The Verilog language does not have enumerated types. To create pseudo labels for data values, it is necessary to define a parameter constant to represent each value, and assign a value to that constant. Alternatively, Verilog’s ‘define text substitution macro can be used to define a set of macro names with specific values for each name.
The following example shows a simple state machine sequence modeled using Verilog parameter constants and ‘define macro
names: The parameters are used to define a set of states for the state machine, and the macro names are used to define a set of instruction words that are decoded by the state machine.
Example 4-3: State machine modeled with Verilog ‘define and parameter constants
`define FETCH 3'h0
`define WRITE 3'h1
`define ADD 3'h2
`define SUB 3'h3
`define MULT 3'h4
`define DIV 3'h5
`define SHIFT 3'h6
`define NOP 3'h7
module controller (output reg read, write,
input wire [2:0] instruction,
input wire clock, resetN);
parameter WAITE = 0,
LOAD = 1,
STORE = 2;
reg [1:0] State, NextState;
always @(posedge clock, negedge resetN)
if (!resetN) State <= WAITE;
else State <= NextState;
always @(State) begin
case (State)
WAITE: NextState = LOAD;
LOAD: NextState = STORE;
STORE: NextState = WAITE;
endcase
end
always @(State, instruction) begin
read = 0; write = 0;
if (State == LOAD && instruction == `FETCH)
read = 1;
else if (State == STORE && instruction == `WRITE)
write = 1;
end
endmodule
constants do not limit the legal set of values
The variables that use the constant values—State and NextState in the preceding example—must be declared as standard Verilog
variable types. This means a software tool cannot limit the valid values of those signals to just the values of the constants. There is nothing that would limit State or NextState in the example above from having a value of 3, or a value with one or more bits set
to X or Z. Therefore, the model itself must add some limit checking on the values. At a minimum, a synthesis “full case” pragma would be required to specify to synthesis tools that the state variable only uses the values of the constants that are listed in the case items. The use of synthesis pragmas, however, would not affect simulation, which could result in mismatches between simulation behavior and the structural design created by synthesis.
SystemVerilog style for labeling values
SystemVerilog adds enumerated type declarations to the Verilog language, using the enum keyword, as in C. In its basic form, the declaration of an enumerated type is similar to C.
enum {WAITE, LOAD, STORE} State, NextState;
enumerated values are identified with labels
Enumerated types can make a model or test program more readable by providing a way to incorporate meaningful labels for the values a variable can have. This can make the code more self-documenting and easier to debug. Enumerated types can be referenced or displayed using the enumerated labels.
Example 4-4 shows the same simple state sequencer as example 4-3, but modified to use SystemVerilog enumerated types.
Example 4-4: State machine modeled with enumerated types
package chip_types;
typedef enum {FETCH, WRITE, ADD, SUB,
MULT, DIV, SHIFT, NOP } instr_t;
endpackage
import chip_types::*; // import package definitions into $unit
module controller (output logic read, write,
input instr_t instruction,
input wire clock, resetN);
enum {WAITE, LOAD, STORE} State, NextState;
always_ff @(posedge clock, negedge resetN)
if (!resetN) State <= WAITE;
else State <= NextState;
always_comb begin
case (State)
WAITE: NextState = LOAD;
LOAD: NextState = STORE;
STORE: NextState = WAITE;
endcase
end
always_comb begin
read = 0; write = 0;
if (State == LOAD && instruction == FETCH)
read = 1;
else if (State == STORE && instruction == WRITE)
write = 1;
end
endmodule
enumerated types limit the legal set of values
In this example, the variables State and NextState can only have the valid values of WAITE, LOAD, and STORE. All software tools will interpret the legal value limits for these enumerated type variables in the same way, including simulation, synthesis and formal verification.
The SystemVerilog specialized always_ff and always_comb procedural blocks used in the preceding example are discussed in more detail in Chapter 6.
Importing enumerated types from packages
NOTE: Importing an enumerated type definition name does not automatically import the enumerated value labels.
When an enumerated type definition is imported from a package, only the typed name is imported. The value labels in the enumerated list are not imported and made visible in the name space in which the enumerated type name is imported. The following code snippet will not work.
package chip_types;
typedef enum {WAITE, LOAD, READY} states_t;
endpackage
module chip (...);
import chip_types::states_t; // imports the typedef name, only
states_t state, next_state;
always_ff @(posedge clk, negedge resetN)
if (!resetN)
state <= WAITE; // ERROR: "WAITE" has not been imported!
else
state <= next_state;
...
endmodule
In order to make the enumerated type labels visible, either each label must be explicitly imported, or the package must be wildcard imported. A wildcard import will make both the enumerated type name and the enumerated value labels visible in the scope of the import statement. For example:
import chip_types::*; // wildcard import
4.2.1 Enumerated type label sequences
In addition to specifying a set of unique labels, SystemVerilog provides two shorthand notations to specify a range of labels in an enumerated type list.
The following example creates an enumerated list with the labels RESET, S0 through S4, and W6 through W9:
enum {RESET, S[5], W[6:9]} state;
4.2.2 Enumerated type label scope
enumerated labels must be unique
The labels within an enumerated type list must be unique within that scope. The scopes that can contain enumerated type declarations are the compilation unit, modules, interfaces, programs, begin...end blocks, fork...join blocks, tasks and functions.
The following code fragment will result in an error, because the enumerated label GO is used twice in the same scope:
module FSM (...);
enum {GO, STOP} fsm1_state;
...
enum {WAITE, GO, DONE} fsm2_state; // ERROR
...
This error in the preceding example can be corrected by placing at least one of the enumerated type declarations in a begin...end
block, which has its own naming scope.
module FSM (...);
...
always @(posedge clock)
begin: fsm1
enum {STOP, GO} fsm1_state;
...
end
always @(posedge clock)
begin: fsm2
enum {WAITE, GO, DONE} fsm2_state;
...
end
...
4.2.3 Enumerated type values
enumerated type labels have a default value
By default, the actual value represented by the label in an enumerated type list is an integer of the int type. The first label in the enumerated list is represented with a value of 0, the second label with a value of 1, the third with a value of 2, and so on.
users can specify the label’s value
SystemVerilog allows the value for each label in the enumerated list to be explicitly declared. This allows the abstract enumerated type to be refined, if needed, to represent more detailed hardware characteristics. For example, a state machine sequence can be explicitly modeled to have one-hot values, one-cold values, Johnson-count, Gray-code, or other type of values.
In the following example, the variable state can have the values ONE, FIVE or TEN. Each label in the enumerated list is represented as an integer value that corresponds to the label.
enum {ONE = 1,
FIVE = 5,
TEN = 10 } state;
It is not necessary to specify the value of each label in the enumerated list. If unspecified, the value representing each label will be incremented by 1 from the previous label. In the next example, the label A is explicitly given a value of 1, B is automatically given the incremented value of 2 and C the incremented value of 3. X is explicitly defined to have a value of 24, and Y and Z are given the incremented values of 25 and 26, respectively.
enum {A=1, B, C, X=24, Y, Z} list1;
label values must be unique
Each label in the enumerated list must have a unique value. An error will result if two labels have the same value. The following example will generate an error, because C and D would have the same value of 3:
enum {A=1, B, C, D=3} list2; // ERROR
4.2.4 Base type of enumerated types
the default base type of an enumerated type is int
Enumerated types are variables or nets with a set of labeled values. As such, enumerated types have a Verilog or SystemVerilog base type. The default base type for enumerated types is int, which is a 32-bit 2-state type.
the base type can be explicitly defined
In order to represent hardware at a more detailed level, SystemVerilog allows an explicit base type for the enumerated types to be declared. For example:
// enumerated type with a 1-bit wide, 2-state base type
enum bit {TRUE, FALSE} Boolean;
// enumerated type with a 2-bit wide, 4-state base type
enum logic [1:0] {WAITE, LOAD, READY} state;
enum value size
If an enumerated label of an explicitly-typed enumerated type is assigned a value, the size must match the size of the base type.
enum logic [2:0] {WAITE = 3’b001,
LOAD = 3’b010,
READY = 3’b100} state;
It is an error to assign a label a value that is a different size than the size declared for the base type of the enumerated type. The following example is incorrect. The enum variable defaults to an int base type. An error will result from assigning a 3-bit value to the labels.
enum {WAITE = 3’b001, // ERROR!
LOAD = 3’b010,
READY = 3’b100} state;
It is also an error to have more labels in the enumerated list than the base type size can represent.
enum logic {A=1’b0, B, C} list5;
// ERROR: too many labels for 1-bit size
4-state enumerated types
If the base type of the enumerated values is a 4-state type, it is legal to assign values of X or Z to the enumerated labels.
enum logic {ON=1’b1, OFF=1’bz} out;
If a value of X or Z is assigned to a label in an enumerated list, the next label must also have an explicit value assigned. It is an error to attempt to have an automatically incremented value following a label that is assigned an X or Z value.
enum logic [1:0]
{WAITE, ERR=2’bxx, LOAD, READY} state;
// ERROR: cannot determine a value for LOAD
4.2.5 Typed and anonymous enumerations
typed enumerated types are defined using typedef
Enumerated types can be declared as a user-defined type. This provides a convenient way to declare several variables or nets with the same enumerated value sets. An enumerated type declared using typedef is commonly referred to as a typed enumerated type. If typedef is not used, the enumerated type is commonly referred to as an anonymous enumerated type.
typedef enum {WAITE, LOAD, READY} states_t;
states_t state, next_state;
4.2.6 Strong typing on enumerated type operations
most variable types are loosely typed
Most Verilog and SystemVerilog variable types are loosely typed, meaning that any value of any type can be assigned to a variable. The value will be automatically converted to the type of the variable, following conversion rules specified in the Verilog or System-Verilog standard.
enumerated types are strongly typed
Enumerated types are the exception to this general nature of Verilog. Enumerated types are semi-strongly typed. An enumerated type can only be assigned:
• A label from its enumerated type list
• Another enumerated type of the same type (that is, declared with the same typedef definition)
• A value cast to the typedef type of the enumerated type
operations use the base type of the label
When an operation is performed on an enumerated type value, the enumerated value is automatically converted to the base type and internal value that represents the label in the enumerated type list. If a base type for the enumerated type is not explicitly declared, the base type and labels will default to int types.
In the following example:
typedef enum {WAITE, LOAD, READY} states_t;
states_t state, next_state;
int foo;
WAITE will be represented as an int with a value of 0, LOAD as an int with a value of 1, and READY as an int value of 2. The following assignment operation on the enumerated type is legal:
state = next_state; // legal operation
The state and next_state are both enumerated type variables of the same type (states_t). A value in one enumerated type variable can be assigned to another enumerated type variable of the same type.
The assignment statement below is also legal. The enumerated type of state is represented as a base type of int, which is added to the literal integer 1. The result of the operation is an int value, which is assigned to a variable of type int.
foo = state + 1; // legal operation
The converse of the preceding example is illegal. An error will result if a value that is not of the same enumerated type is assigned to an enumerated type variable. For example:
state = foo + 1; // ERROR: illegal assignment
In this example, the resulting type of foo + 1 is an int, which is not the same type as state, which is a states_t type.
The next examples are also illegal, and will result in errors:
state = state + 1; // illegal operation
state++; // illegal operation
next_state += state; // illegal operation
The enumerated type of state is represented as a base type of int, which is added to the literal integer 1. The result of the operation is an int value. It is an error to directly assign this int result to a variable of the enumerated type state, which is a states_t type.
4.2.7 Casting expressions to enumerated types
casting values to an enumerated type
The result of an operation can be cast to a typed enumerated type, and then assigned to an enumerated type variable of the same type. Either the SystemVerilog cast operator or the dynamic $cast system function can be used (see section 3.9 on page 67 in Chapter 3).
typedef enum {WAITE, LOAD, READY} states_t;
states_t state, next_state;
next_state = states_t’(state++); // legal
$cast(next_state, state + 1); // legal
using the cast operator
As discussed earlier in section 3.9 on page 67, there is an important distinction between using the cast operator and the dynamic $cast system function. The cast operator will always perform the cast operation and assignment. There is no checking that the value to be assigned is in the legal range of the enumerated type set. Using the preceding enumerated type example for state and next_state, if state had a value of READY, which is represented as a value of 2, incrementing it by one would result in an integer value of 3. Assigning this value to next_state is out of the range of values within the enumerated type list for next_state.
This out-of-range value can result in indeterminate behavior. Different software tools may do different things with the out-of-range value. If an out-of-range value is assigned, the actual value that might end up stored in the enumerated type during pre-synthesis simulation of the RTL model might be different than the functionality of the gate-level netlist generated by synthesis.
To avoid ambiguous behavior, it is important that a model be coded so that an out-of-range value is never assigned to an enumerated type variable. The static cast operator cannot always detect when an out-of-range value will be assigned, because the cast operator does not do run-time error checking.
using the $cast system function
The dynamic $cast
system function verifies that the expression result is a legal value before changing the destination variable. In the preceding example, if the result of incrementing state is out-of-range for next_state, then the call to $cast(next_state, state+1) will not change next_state, and a run-time error will
be reported.
The two ways to perform a cast allow the modeler to make an intelligent trade-off in modeling styles. The dynamic cast is safe because of its run-time error checking. However, this run-time checking adds some amount of processing overhead to the operation, which can affect software tool performance. Also, the $cast system function may not be synthesizable. The compile-time cast operator does not perform run-time checking, allowing the cast operation to be optimized for better run-time performance.
Users can choose which casting method to use, based on the nature of the model. If it is known that out-of-range values will not occur, the faster compile-time cast operator can be used. If there is the possibility of out-of-range values, then the safer $cast system function can be used. Note that the SystemVerilog assert statement can also be used to catch out-of-range values, but an assertion will not prevent the out-of-range assignment from taking place. Assertions are discussed in the companion book, SystemVerilog for Verification1.
4.2.8 Special system tasks and methods for enumerated types
iterating through the enumerated type list
SystemVerilog provides several built-in functions, referred to as methods, to iterate through the values in an enumerated type list. These methods automatically handle the semi-strongly typed nature of enumerated types, making it easy to do things such as increment to the next value in the enumerated type list, jump to the beginning of the list, or jump to the end of the list. Using these methods, it is not necessary to know the labels or values within the enumerated list.
enumerated type methods use a C++ syntax
These special methods for working with enumerated lists are called in a manner similar to C++ class methods. That is, the name of the method is appended to the end of the enumerated type name, with a period as a separator.
<enum_variable_name>.first — returns the value of the first member in the enumerated list of the specified variable.
<enum_variable_name>.last — returns the value of the last member in the enumerated list.
<enum_variable_name>.next(<N>) — returns the value of the next member in the enumerated list. Optionally, an integer value can be specified as an argument to next. In this case, the Nth next value in the enumerated list is returned, starting from the position of the current value of the enumerated type. When the end of the enumerated list is reached, a wrap to the start of the list occurs. If the current value of the enumerated type is not a member of the enumerated list, the value of the first member in the list is returned.
<enum_variable_name>.prev(<N>) — returns the value of the previous member in the enumerated list. As with the next method, an optional integer value can be specified as an argument to prev. In this case, the Nth previous value in the enumerated list is returned, starting from the position of the current value of the enumerated type. When the beginning of the enumerated list is reached, a wrap to the end of the list occurs. If the current value of the enumerated type is not a member of the enumerated list, the value of the last member is returned.
<enum_variable_name>.num — returns the number of labels in the enumerated list of the given variable.
<enum_variable_name>.name — returns the string representation of the label for the value in the given enumerated type. If the value is not a member of the enumeration, the name method returns an empty string.
Example 4-5 illustrates a state machine model that sequences through its states, using some of the enumeration methods listed above. The example is a simple 0 to 15 confidence counter, where:
• The in_sync output is initially 0; it is set when the counter reaches 8; in_sync is cleared again if the counter goes to 0.
• If the compare and synced input flags are both false, the counter stays at its current count.
• If the compare flag and the synced flag are both true, the counter increments by 1 (but cannot go beyond 15).
• If the compare flag is true but the synced flag is false, the counter decrements by 2 (but cannot go below 0).
Example 4-5: Using special methods to iterate through enumerated type lists
module confidence_counter(input logic synced, compare,
resetN, clock,
output logic in_sync);
enum {cnt[0:15]} State, Next;
always_ff @(posedge clock, negedge resetN)
if (!resetN) State <= cnt0;
else State <= Next;
always_comb begin
Next = State; // default NextState value
case (State)
cnt0 : if (compare && synced) Next = State.next;
cnt1 : begin
if (compare && synced) Next = State.next;
if (compare && !synced) Next = State.first;
end
cnt15: if (compare && !synced) Next = State.prev(2);
default begin
if (compare && synced) Next = State.next;
if (compare && !synced) Next = State.prev(2);
end
endcase
end
always_ff @(posedge clock, negedge resetN)
if (!resetN) in_sync <= 0;
else begin
if (State == cnt8) in_sync <= 1;
if (State == cnt0) in_sync <= 0;
end
The preceding example uses SystemVerilog’s specialized procedural blocks, always_ff and always_comb. These procedural blocks are discussed in more detail in Chapter 6.
4.2.9 Printing enumerated types
printing enumerated type values and labels
Enumerated type values can be printed as either the internal value of the label, or as the name of the label. Printing the enumerated type directly will print the internal value of the enumerated type. The name of the label representing the current value is accessed using the enumerated type name method. This method returns a string containing the name. This string can then be passed to $display for printing.
Example 4-6: Printing enumerated types by value and by name
module FSM (input logic clock, resetN,
output logic [3:0] control);
enum logic [2:0] {WAITE=3'b001,
LOAD =3'b010,
READY=3'b010} State, Next;
always @(posedge clock, negedge resetN)
if (!resetN) State <= WAITE;
else State <= Next;
always_comb begin
$display("\nCurrent state is %s (%b)", State.name, State);
case (State)
WAITE: Next = LOAD;
LOAD: Next = READY;
READY: Next = WAITE;
endcase
$display("Next state will be %s (%b)", Next.name, Next);
end
assign control = State;
endmodule
4.3 Summary
The C-like typedef definition allows users to define new types built up from the predefined types or other user-defined types in Verilog and SystemVerilog. User-defined types can be used as module ports and passed in/out of tasks and functions.
Enumerated types allow the declaration of variables with a limited set of valid values, and the representation of those values with abstract labels instead of hardware-centric logic values. Enumerated types allow modeling a more abstract level than Verilog, making it possible to model larger designs with fewer lines of code. Hardware implementation details can be added to enumerated type declarations, if desired, such as assigning 1-hot encoding values to an enumerated type list that represents state machine states.
SystemVerilog also adds a class type, enabling an object-oriented style of modeling. Class objects and object-oriented programming are primarily intended for verification, and are not currently synthesizable. Details and examples of SystemVerilog classes can be found in the companion book, SystemVerilog for Verification1.