SystemVerilog for Design Edition 2 Chapter 8

发布时间 2023-06-16 23:23:51作者: sasasatori

SystemVerilog for Design Edition 2 Chapter 8

SystemVerilog enables modeling at a higher level of abstraction through the use of 2-state types, enumerated types, and userdefined types. These are complemented by new specialized always procedural blocks, always_comb, always_ff and always_latch. These and other new modeling constructs have been discussed in the previous chapters of this book.

This chapter shows how to use these new levels of model abstractions to effectively model logic such as finite state machines, using a combination of enumerated types and the procedural constructs presented in the previous chapters. Using SystemVerilog, the coding of finite state machines can be simplified and made easier to read and maintain. At the same time, the consistency of how different software tools interpret the Verilog models can be increased.

The SystemVerilog features presented in this chapter include:

• Using enumerated types for modeling Finite State Machines

• Using enumerated types with FSM case statements

• Using always_comb with FSM case statements

• Modeling reset logic with enumerated types and 2-state types

8.1 Modeling state machines with enumerated types

Section 4.2 on page 79 introduced the enumerated type construct that SystemVerilog adds to the Verilog language. This section provides additional guidelines on using enumerated types for modeling hardware logic such as finite state machines.

enumerated types have restricted values

Enumerated types provide a means for defining a variable that has a restricted set of legal values. The values are represented with labels instead of digital logic values.

enumerated types allow abstract FSM models

Enumerated types allow modeling at a higher level of abstraction, and yet still represent accurate, synthesizable, hardware behavior. Example 8-1, which follows, models a simple finite state machine (FSM), using a typical three-procedural block modeling style: one procedural block for incrementing the state machine, one procedural block to determine the next state, and one procedural block to set the state machine output values. The example illustrates a simple traffic light controller. The three possible states are represented as enumerated type variables for the current state and the next state of the state machine.

By using enumerated types, the only possible values of the State and Next variables are the ones listed in their enumerated type lists. The unique modifier to the case statements in the state machine logic helps confirm that the case statements cover all possible values of the State and Next variables (unique case statements are discussed in more detail in section 7.9.1 on page 196).

Example 8-1: A finite state machine modeled with enumerated types (poor style)

module traffic_light (output logic green_light,
								   yellow_light,
								   red_light,
					  input sensor,
					  input [15:0] green_downcnt,
								   yellow_downcnt,
								   input clock, resetN);
enum {RED, GREEN, YELLOW} State, Next; // using enum defaults

always_ff @(posedge clock, negedge resetN)
	if (!resetN) State <= RED; // reset to red light
	else State <= Next;

always_comb begin: set_next_state
	Next = State; // the default for each branch below
	unique case (State)
		RED: if (sensor) Next = GREEN;
		GREEN: if (green_downcnt == 0) Next = YELLOW;
		YELLOW: if (yellow_downcnt == 0) Next = RED;
	endcase
end: set_next_state

always_comb begin: set_outputs
	{green_light, yellow_light, red_light} = 3'b000;
	unique case (State)
		RED: red_light = 1'b1;
		GREEN: green_light = 1'b1;
		YELLOW: yellow_light = 1'b1;
	endcase
end: set_outputs

endmodule

Example 8-1, while functionally correct, might not be a good usage of enumerated types for representing hardware. The example uses the default enum base type of int, and the default values for each enumerated value label (0, 1 and 2, respectively). These defaults might not accurately reflect hardware behavior in simulation. The int type is a 32-bit 2-state type. The actual hardware for the example above, which has only three states, only needs a 2- or 3-bit vector, depending on how the three states are encoded. The gate-level model of the actual hardware implementation will have 4-state semantics.

The default initial value of 2-state types in simulation can hide design problems. This topic is discussed in more detail later in this chapter, in section 8.2 on page 219. The default values of the enumerated labels can also lead to mismatches in the RTL simulation versus the gate-level implementation of the design. Since the values for the enumerated labels were not explicitly specified, synthesis compilers might optimize the gate-level implementation to different values for each state. This makes it more difficult to compare the pre- and post-synthesis model functionality, or to specify assertions that work with both the pre- and post-synthesis models.

8.1.1 Representing state encoding with enumerated types

enumerated types can have an explicit base type

SystemVerilog also allows the base type of an enumerated variable to be defined. This allows a 4-state type, such as logic, to be used as a base type, which can more accurately represent hardware behavior in RTL simulations.

enumerated type labels can have explicit values

SystemVerilog’s enumerated types also allow modeling at a more hardware-like level of abstraction, so that specific state machine architectures can be represented. The logic value of each label in an enumerated type list can be specified. This allows explicitly representing one-hot, one-cold, Gray code, or any other type of state sequence encoding desired.

Example 8-2 modifies the preceding example to explicitly represent one-hot encoding in the state sequencing. The only change between example 8-1 and example 8-2 is the definition of the enumerated type. The rest of the state machine logic remains at an abstract level, using the labels of the enumerated values.

Example 8-2: Specifying one-hot encoding with enumerated types

module traffic_light (output logic green_light,
			                 yellow_light,
							 red_light,
					  input sensor,
					  input [15:0] green_downcnt,
								   yellow_downcnt,
					  input clock, resetN);
					  
enum logic [2:0] {RED = 3'b001, // explicit enum definition
				  GREEN = 3'b010,
				  YELLOW = 3'b100} State, Next;
				  
always_ff @(posedge clock, negedge resetN)
	if (!resetN) State <= RED; // reset to red light
	else State <= Next;

always_comb begin: set_next_state
	Next = State; // the default for each branch below
	unique case (State)
		RED: if (sensor) Next = GREEN;
		GREEN: if (green_downcnt == 0) Next = YELLOW;
		YELLOW: if (yellow_downcnt == 0) Next = RED;
	endcase
end: set_next_state

always_comb begin: set_outputs
	{green_light, yellow_light, red_light} = 3'b000;
	unique case (State)
		RED: red_light = 1'b1;
		GREEN: green_light = 1'b1;
		YELLOW: yellow_light = 1'b1;
	endcase
end: set_outputs

endmodule

In this example, the enumerated label values that represent the state sequencing are explicitly specified in the RTL model. Synthesis compilers will retain these values in the gate-level implementation. This helps in comparing pre- and post-synthesis model functionality. It also makes in easier to specify verification assertions that work with both the pre- and post-synthesis models. (Synthesis compiler may provide a way to override the explicit enumeration label values, in order to optimize the gate-level implementation; This type of optimization cancels many of the benefits of specifying explicit enumeration values).

Another advantage illustrated in the example above is that the base type of the enumerated State and Next variables is a 4-state logic data type. The default initial value of 4-state types is X instead of 0. Should the design not implement reset correctly, it will be obvious in the RTL simulation that there is a design problem. This topic is discussed in more detail later in this chapter, in section 8.2 on page 219.

8.1.2 Reversed case statements with enumerated types

The typical use of a case statement is to specify a variable as the case expression, and then list explicit values to be matched as the list of case selection items. This is the modeling style shown in the previous two examples.

one-hot state machines can use reversed case statements

Another style for modeling one-hot state machines is the reversed case statement. In this style, the case expression and the case selection items are reversed. The case expression is specified as the literal value to be matched, which, for one-hot state machines, is a 1-bit value of 1. The case selection items are each bit of the state variable. In some synthesis compilers, using the reversed case style for one-hot state machines might yield more optimized synthesis results than the standard style of case statements.

Example 8-3 illustrates using a reversed case statement style. In this example, a second enumerated type variable is declared that represents the index number for each bit of the one-hot State register. The name R_BIT, for example, has a value of 0, which corresponds to bit 0 of the State variable (the bit that represents the RED state).

Example 8-3: One-hot encoding with reversed case statement style

module traffic_light (output logic green_light,
								   yellow_light,
                      			   red_light,
					  input 	   sensor,
					  input [15:0] green_downcnt,
								   yellow_downcnt,
					  input 	   clock, resetN);

enum {R_BIT = 0, // index of RED state in State register
	  G_BIT = 1, // index of GREEN state in State register
	  Y_BIT = 2} state_bit;

// shift a 1 to the bit that represents each state
enum logic [2:0] {RED = 3'b001<<R_BIT,
				  GREEN = 3'b001<<G_BIT,
				  YELLOW = 3'b001<<Y_BIT} State, Next;

always_ff @(posedge clock, negedge resetN)
	if (!resetN) State <= RED; // reset to red light
	else State <= Next;

always_comb begin: set_next_state
	Next = State; // the default for each branch below
	unique case (1'b1) // reversed case statement
		State[R_BIT]: if (sensor) Next = GREEN;
		State[G_BIT]: if (green_downcnt == 0) Next = YELLOW;
		State[Y_BIT]: if (yellow_downcnt == 0) Next = RED;
	endcase
end: set_next_state

always_comb begin: set_outputs
	{red_light, green_light, yellow_light} = 3'b000;
	unique case (1'b1) // reversed case statement
		State[R_BIT]: red_light = 1'b1;
		State[G_BIT]: green_light = 1'b1;
		State[Y_BIT]: yellow_light = 1'b1;
	endcase
end: set_outputs

endmodule

a clever coding trick for using enumerated types with 1-hot FSM models

In the example above, the enumerated variable state_bit specifies which bit of the state sequencer represents each state (the 1-hot bit). The value for each state label is calculated by shifting a 3-bit value of 001 (binary) to the bit position that is “hot” for that state. A value of 001 shifted 0 times (the value of R_BIT) is 001 (binary). A 001 shifted 1 time (the value of G_BIT) is 010 (binary), and shifted 2 times (the value of Y_BIT) is 100 (binary).

The same enumerated state_bit labels, R_BIT, G_BIT and Y_BIT, are used in the functional code to test which bit of State is “hot”. Thus, the definitions of the enumerated labels for State and the bit-selects of the State variable are linked together by the definition of state_bit. Using this seemingly complex scheme to specify the 1-hot state values serves two important purposes:

• There is no possibility of a coding error that defines different 1- hot bit positions in the two enumerated type definitions.

• Should the design specification change the 1-hot definitions, only the enumerated type specifying the bit positions has to change. The enumerated type defining the state names will automatically reflect the change.

This clever coding trick of using the bit-shift operator to specify the enumerated values of the state variables was shared by Cliff Cummings of Sunburst Design. Additional FSM coding tricks can be found at Cliff’s web site, www.sunburst-design.com.

8.1.3 Enumerated types and unique case statements

unique case reduces the ambiguities of case statements

The use of the unique modifier to the case statement in the preceding example is important. Since a one-hot state machine only has one bit of the state register set at a time, only one of the case selection items will match the literal value of 1 in the case expression. The unique modifier to the case statement specifies three things.

First, unique case specifies that all case selection items can be evaluated in parallel, without priority encoding. Software tools such as synthesis compilers can optimize the decoding logic of the case selection items to create smaller, more efficient implementations. This aspect of unique case is the same as synthesis parallel_case pragma.

Second, unique case specifies that there should be no overlap in the case selection items. During the run-time execution of tools such as simulation, if the value of the case expression satisfies two or more case selection items, a run-time warning will occur. This semantic check can help trap design errors early in the design process. The synthesis parallel_case pragma does not provide this important semantic check.

Third, unique case specifies that all values of the case expression that occur during simulation must be covered by the case selection items. With unique case, if a case expression value occurs that does not cause a branch of the case statement to be executed, a runtime warning will occur. This semantic check can also help trap design errors much earlier in the design cycle. This is similar to the full_case pragma for synthesis, but the synthesis pragma does not require that other tools perform any checking.

8.1.4 Specifying unused state values

Verilog types can have unused values

As an enumerated type, the State variable has a restricted set of values. The State variable is a multi-bit vector, which, at the gatelevel, can reflect logic values not defined in the enumerated list. A finite state machine with three states requires a 3-bit state register for one-hot encoding. This 3-bit register can contain 8 possible values. The hardware registers represented can hold all possible values, not just the values listed in the enumerated list. The base type of the enumerated type can also represent all 8 of these values.

There are two common modeling styles to indicate that some values of the case expression are not used: specify a default case selection with a logic X assignment, or specify a special synthesis full_case pragma. These two styles are discussed in more detail in the following paragraphs.

Using X as a default assignment

The combination of enumerated types and unique case can eliminate the need for a common Verilog coding style with case statements. This Verilog style is to specify a default statement to cover all unused values of the case expression. This default statement assigns a logic X to the variables representing the outputs of the case statement. In the FSM example from above, the case expression is the current state variable, State, and the output of the case statement is the next state variable, Next.

// Verilog style case statement with X default
reg [2:0] State, Next; // 3-bit variables
case (State)
	3'b001: Next = 3'b010;
	3'b010: Next = 3'b100;
	3'b100: Next = 3'b001;
	default: Next = 3'bXXX;
endcase

Synthesis compilers recognize the default assignment of logic X as an indication that any case expression value that falls into the default case is an unused value. This can enable the synthesis compiler to perform additional optimizations and improve the synthesis quality of results.

enumerated types cannot be directly assigned an X value

When enumerated types are used, an assignment of logic X is not a legal assignment. An enumerated type can only be assigned values from its enumerated list. If an X assignment is desired, the base type of the enumerated type must be defined a 4-state type, such as logic, and an enumerated label must be defined with an explicit value of X. For example:

// case statement with enumerated X default
enum logic [2:0] {RED = 3'b001,
				  GREEN = 3'b010,
				  YELLOW = 3'b100,
				  BAD_STATE = 3'bxxx,
} State, Next;

case (State)
	RED: Next = GREEN;
	GREEN: Next = YELLOW;
	YELLOW: Next = RED;
	default: Next = BAD_STATE;
endcase

enumerated types can eliminate unused conditions

With SystemVerilog, the BAD_STATE enumerated value and the default case item are not needed. The combination of enumerated types and unique case statements eliminates the need for using a logic X assignment to show that not all case expression values are used. The enumerated type limits the values of its variables to just the values listed in the enumerated value set. These are the only values that need to be listed in the case statement. The defined set of values that an enumerated type can hold, along with the additional unique case semantic checking (discussed in section 8.1.3 on page 213) help ensure that pre-synthesis RTL model and the postsynthesis gate-level model are the same for both simulation and equivalence checking.

As discussed in the preceding paragraphs, using unique case combines the functionality of both the synthesis parallel_case and full_case pragmas. The unique case also provides semantic checks to ensure that all values of an enumerated type used as a case expression truly meet the requirements to be implemented as parallel, combinational logic. Any unintended or unexpected case expression values will be trapped as run-time warnings by a unique case statement.

8.1.5 Assigning state values to enumerated type variables

enumerated types can only be assigned values in their type set

Enumerated types are more strongly typed than other Verilog and SystemVerilog variables. Enumerated types can only be assigned a value that is a member of the type list of that enumerated type. An enumerated type can be assigned the value of another enumerated type, but only if both enumerated types are from the same definition. Section 4.2.6 on page 86 of Chapter 4, discusses the assignment rules for enumerated types in more details.

A common Verilog style when using one-hot state sequences is to first clear the next state variable, and then set just the one bit of next state variable that indicates what the next state will be. This style will not work with enumerated types. Consider the following code snippet:

Example 8-4: Code snippet with illegal assignments to enumerated types

enum {R_BIT = 0, // index of RED state in State register
	  G_BIT = 1, // index of GREEN state in State register
	  Y_BIT = 2} state_bit;

// shift a 1 to the bit that represents each state
enum logic [2:0] {RED = 3'b001<<R_BIT,
				  GREEN = 3'b001<<G_BIT,
				  YELLOW = 3'b001<<Y_BIT} State, Next;
...

always_comb begin: set_next_state
	Next = 3’b000; // clear Next - ERROR: ILLEGAL ASSIGNMENT
	unique case (1’b1) // reversed case statement
	// WARNING: FOLLOWING ASSIGNMENTS ARE POTENTIAL DESIGN ERRORS
		State[R_BIT]: if (sensor == 1) Next[G_BIT] = 1’b1;
		State[G_BIT]: if (green_downcnt==0) Next[Y_BIT] = 1’b1;
		State[Y_BIT]: if (yellow_downcnt==0) Next[R_BIT] = 1’b1;
	endcase
end: set_next_state
...

There are two problems with the code snippet above. First, a default assignment of all zeros is made to the Next variable. This is an illegal assignment. An enumerated type must be assigned labels from its enumerated list, not literal values.

Second, within the case statements, assignments are made to individual bits of the Next variable. Assigning to a discrete bit of an enumerated type may be allowed by compilers, but it is not a good style when using enumerated types. By assigning to a bit of an enumerated type variable, an illegal value could be created that is not in the enumerated type list. This would result in design errors that could be difficult to debug.

TIP: Assign an enumerated type variable a label from its enumerated list, instead of a value.

Assignments to enumerated type variables should be from the list of labels for that type. Assigning to bit-selects or part-selects of an enumerated type should be avoided. When assignments to bits of a variable are required, the variable should be declared as standard type, such as bit or logic, instead of an enumerated type. Example 8-3 on page 212 shows the correct way to model a Verilog “reverse case statement” when using enumerated types.

8.1.6 Performing operations on enumerated type variables

Enumerated types differ from most other Verilog types in that they are strongly typed variables. For example, it is illegal to directly assign a literal value to an enumerated type. When an operation is performed on an enumerated type variable, the value of the variable is the type of the base type of the enumerated type. By default, this is an int type, but can be explicitly declared as other types.

The following example will result in an error. The operation State + 1 will result in an int value. Directly assigning this int value to the Next variable, which is an enumerated type variable, is illegal.

enum {RED, GREEN, YELLOW} State, Next;
Next = State + 1; // ILLEGAL ASSIGNMENT

A value of a different type can be assigned to an enumerated type using type casting. SystemVerilog provides both a static cast operator and a dynamic cast system function.

typedef enum {RED, GREEN, YELLOW} states_t;
states_t State, Next;
Next = states_t’(State + 1); // static cast
$cast(Next, State + 1); // dynamic cast

A static cast operation coerces an expression to a new type without performing any checking on whether the value coerced is a valid value for the new type. If, for example, the current value of State were YELLOW, then State + 1 would result in an out-of-bounds value. Using static casting, this out-of-bounds value would not be trapped. The SystemVerilog standard allows software tools to handle out-of-bounds assignments in a nondeterministic manner. This means the new value of the Next variable in the preceding static cast assignment could, and likely will, have different values in different software tools.

A dynamic cast performs run-time checking on the value being cast. If the value is out-of-range, then an error message is generated, and the target variable is not changed. By using dynamic casting, inadvertent design errors can be trapped, and the design corrected to prevent the out-of-bounds values.

SystemVerilog also provides a number of special enumerated type methods for performing basic operations on enumerated type variables. These methods allow incrementing or decrementing a value within the list of legal values for the enumerated type.

Next = State.next; // enumerated method

Section 4.2.8 on page 89 discusses the various enumerated methods in more detail.

Each of these styles of assigning the result of an operation to an enumerated type has advantages. Using the enumerated type methods ensures the assigned value will always be within the set of values in the enumerated type list. The dynamic cast operator provides run-time errors for out-of-range values. Static casting does not perform any error checking, but might yield better simulation run-time performance compared to using methods or dynamic casting. With static casting, however, the burden is on the designer to ensure that an out-of-bound value will never occur.

8.2 Using 2-state types in FSM models

8.2.1 Resetting FSMs with 2-state and enumerated types

At the beginning of simulation, 4-state types are logic X. Within a model such as a finite state machine, a logic X on 4-state variables can serve as an indication that the model has not been reset, or that the reset logic has not been properly modeled.

2-state types begin simulation with a default value of logic 0 instead of an X. Since the typical action of reset is to set most variables to 0, it can appear that the model has been reset, even if there is faulty reset logic.

Enumerated types begin simulation with a default value of the base type of the enumerated type. If the state variables are defined using the default base type and label values, and if reset also sets enumerated values to the first item in the list, then a similar situation can occur as with 2-state variables. The default base type is int, which has an un-initialized value of 0 at the beginning of simulation. The default value for the first label in an enumerated list is 0, which is the same as the un-initialized value of the 2-state base type. The design can appear to have been reset, even if reset is never asserted, or if the design reset logic has errors.

The following example will lock-up in the WAITE state. This is because both the State and Next variables begin simulation with a value of 0, which is also the value of the first value in their enumerated lists, WAITE. At every positive edge of clock, State is assigned the value it already has, and therefore no transition occurs. Since there is no transition, the always @(State) procedural block that decodes Next is not triggered, and therefore Next is not changed from its initial value of WAITE.

enum {WAITE, LOAD, STORE} State, Next;

always @(posedge clock, negedge resetN)
	if (!resetN) State <= WAITE;
	else State <= Next;

always @(State)
	case (State)
		WAITE: Next = LOAD;
		LOAD: Next = STORE;
		STORE: Next = WAITE;
	endcase

Applying reset does not fix this state lock-up problem. Reset changes the State variable to WAITE, which is the same value that State begins simulation with. Therefore there is no change to the State variable and the next state decode logic is not triggered. Next continues to keep its initial value, which is also WAITE.

This lock-up at the start of simulation can be fixed in two ways. The first way is to explicitly declare the enumerated variable with a 4-state base type, such as logic. Simulation will then begin with State and Next having an un-initialized value of X. This is a clear indication that these variables have been reset. It also more accurately reflects the nature of hardware, where flip-flops can power up in an indeterminate state. In RTL simulation, when reset is applied, the State variable will transition from X to its reset value of WAITE. This transition will trigger the logic that decodes Next, setting Next to its appropriate value of LOAD.

The second fix for the FSM lock up, when using an enumerated type with its default base type and label values, is to replace always @(state) with the SystemVerilog always_comb procedural block. Analways_comb procedural block automatically executes its statements once at simulation time zero, even if there were no transitions on its inferred sensitivity list. By executing the decode logic at time zero, the initial value of State will be decoded, and the Next variable set accordingly. This fixes the start of simulation lock-up problem.

Combining unique case along with the use of a 4-state base type for enumerated types also has an advantage. If State in the code snippet above had not been reset, it would be a logic X, which will not match any of the case items to which State is compared. The unique case (State) statement will issue a run-time warning whenever no case items match the case expression. (A warning would also be issued if the case expression matches more than one case item.)

Examples 8-2 on page 210 and 8-3 on page 212 illustrate using 4-state enumerated types coupled with always_comb and unique case. This combination of SystemVerilog constructs not only simplifies writing RTL code, it can trap design problems that in Verilog could have been difficult to detect and debug.

8.3 Summary

This chapter has presented suggestions on modeling techniques when representing hardware behavior at a more abstract level. SystemVerilog provides several enhancements that enable accurately modeling designs that simulate and synthesize correctly. These enhancements help to ensure consistent model behavior across all software tools, including lint checkers, simulators, synthesis compilers, formal verifiers, and equivalence checkers.

Several ideas were presented in this section on how to properly model finite state machines using these new abstract modeling constructs such as: 2-state types, enumerated types, always_comb procedural blocks, and unique case statements.