A SystemVerilog style guide.
- Packages,
import
, andinclude
always
,always_ff
, andalways_comb
wire
,reg
, andlogic
enum
Types- Blocking and Non-blocking Assignment
Using include
to share code with other files should be avoided.
`include "sample_pkg.sv"
The include
keyword creates a new definition of the code sample_pkg.sv
each time it is used. This because include
is a macro rather than a proper import, and simply "copies" the text contents of sample_pkg.sv
into the file. This creates several issues, such as when using custom types as ports for modules or as the arguments of functions, as you have conflicting between the top-level/caller and the module/callee.
include
is a hold-over from Verilog, and should be replaced with packages and import
in SystemVerilog.
Packages can be defined in the following manner:
package sample_pkg;
// Definitions here...
endpackage
Symbols in packages can either be referenced using the package name
assign value = sample_pkg::SAMPLE_CONSTANT;
or by importing either individual symbols from the package
import sample_pkg::SAMPLE_CONSTANT;
...
assign value = SAMPLE_CONSTANT;
or the entire package
import sample_pkg::*;
...
assign value = SAMPLE_CONSTANT;
Using always
for sequential and combinational logic should be avoided.
// Old Verilog-style sequential block.
always @(posedge clk)
begin
...
end
// Old Verilog-style combinational block.
always @(*)
begin
...
end
Verilog-style always
are not checked at compile time to verify that the code included synthesizes to true flip-flops or combinational logic. Warnings are often generated in synthesis tools when code does not map properly to flip-flops or combinational logic, but are often missed, and simulation tools will often remain silent.
Use always_ff
and always_comb
instead. always_ff
and always_comb
blocks are verified by most tool chains at compile time and will throw errors if the code does not map to flip-flops or combinational logic. For example, using always_comb
will cause compilation to fail if the code infers a latch while always @(*)
will only throw a warning.
// New SystemVerilog-style sequential block.
always_ff @(posedge clk)
begin
...
end
// New SystemVerilog-style combinational block.
always_comb
begin
...
end
Using wire
and reg
should be avoided.
wire internal_wire;
reg internal_reg;
...
assign internal_wire = value_0;
always @(*)
begin
internal_reg = value_1;
end
The common convention in Verilog is to use wire
for nodes driven outside of always
blocks (such as those driven via assign
), and reg
for nodes driven in always
blocks. This is convention is tedious and makes refactoring code difficult. It also fails to add any clarity, since reg
could be a node driven by either combinational or sequential logic.
Use logic
instead of wire
and reg
. Nodes of type logic
can be driven either inside or outside of always
blocks.
logic node_0;
logic node_1;
...
assign node_0 = value_0;
always_comb
begin
node_1 = value_1;
end
It is also good practice to use logic
for all output port, as it also allows them to be driven either inside or outside of always
blocks.
module sample_module (
output logic output_0,
output logic output_1
);
...
assign output_0 = value_0;
always_comb
begin
output_1 = value_1;
end
endmodule
Avoid using non-enum
types (parameter
, logic
, int
, etc) when defining state machine states.
parameter [2:0] IDLE = 3'b001, RUN = 3'b010, DONE = 3'b001; // Functional bug
Here we have unintentionally defined multiple states (IDLE
and DONE
) with the same value (3'b001
), causing the state machine to fail.
parameter [1:0] IDLE = 3'b001, RUN = 3'b010, DONE = 3'b100; // Functional bug
Here we have unintentionally assigned 3-bit values to 2-bit states. Since Verilog allows width mismatches in assignments, the code compiles, and the state machine will fail. This case is especially common when additional states are added to an existing state machine.
parameter [2:0] IDLE = 3'b001, RUN = 3'b010, DONE = 3'b100;
wire [2:0] next_state;
assign next_state = 3'b111; // Functional bug
Here we have unintentionally assigned an invalid state value to next_state
. Once again, this compiles and the state machine will fail.
Use enum
types for defining states in state machines.
typedef enum logic [2:0] {IDLE = 3'b001, RUN = 3'b010, DONE = 3'b001} state_t; // Compile error
state_t state;
state_t next_state;
As before, we have unintentionally defined multiple states (IDLE
and DONE
) with the same value (3'b001
). However, this time it causes a compile error because enum
types require the entire enumerated list to be unique.
typedef enum logic [1:0] {IDLE = 3'b001, RUN = 3'b010, DONE = 3'b100} state_t; // Compile error
state_t state;
state_t next_state;
As before, we have unintentionally assigned 3-bit values to 2-bit states. However, this time it causes a compile error because enum
types require the values assigned to the enumerated list to match the width of the type.
typedef enum logic [2:0] {IDLE = 3'b001, RUN = 3'b010, DONE = 3'b100} state_t; // Compile error
state_t state;
state_t next_state;
assign next_state = 3'b111;
As before, we have unintentionally assigned an invalid state value to next_state
. However, since an enum
type node can only be assigned labels (IDLE
, RUN
, DONE
) or values (3'b001
, 3'b010
, 3'b100
) from its enumerated list, the code will fail to compile.
We may also choose to leave label values unassigned:
typedef enum logic [2:0] {IDLE, RUN, DONE} state_t;
state_t state;
state_t next_state;
assign next_state = IDLE;
Note that this means we can no longer assign enum
type nodes by value: we must assign them by label (IDLE
, RUN
, DONE
). This often increases readability (think of it as using constants rather than literals) and maitainability (adding additional states is as simple as adding a new label: we do not need to manage all the values by hand).
Simulators often allow you to view enum
type nodes by label (IDLE
, RUN
, DONE
) instead of value (3'b001
, 3'b010
, 3'b100
) in waveform view, allowing for easier debugging.
Avoid using blocking assignment (=
) in always_ff
blocks as it creates an inconsistencies when referencing the assigned node.
always_ff @(posedge clk)
begin
c = a + b;
e_in <= c; // The c referenced here is before the flip flop.
...
end
assign e_out = c; // The c referenced here is after the flip flop.
If a node assigned using blocking assignment is referenced inside the same always_ff
block it was assigned in after the assignment, it takes the value of the node before the flip flop. If referenced anywhere else, it takes the value of the node after the flip flop. Blocking assignment creates two separate nodes with the same name, and which one you get depends on where you reference it. This is both functionally confusing and results in poor maintainability.