Modules, Controls, and Interfaces
3.1 Modules
Modules are the basic building blocks of SystemVerilog. It is intended to be a reusable component that can be connected to form a larger component. To declare a module, we can use the following syntax:
Notice that due to legacy reason, there is no namespace for modules. As a result, module_name
has to be unique in your entire design. To declare the ports for the module, we can simply do
module module_name (input logic clk,
input logic rst_n,
input logic[7:0] in,
output logic[7:0] out);
// content of the module
endmodule
Keywords input
and output
are used to specify the direction of ports. There is another keyword, inout
which makes the port bidirectional. inout
is typically used for tri-state designs and we will not cover it in the book. If you are declaring multiple ports sharing the same port direction and types, you can omit the subsequential ones, as shown below. Notice that the code is equivalent. It is up to the designers to choose which style they want to follow. In this book we will use the more verbose version.
The type for the ports can be any integral values, such as logic, arrays, or struct. It can also be interface, which will be covered later in the chapter.
Notice that there is another style of declaring port, which is specifying port names first, then later on declare the port direction and types, typically called Non-ANSI style. This style is out-dated and we do not recommend use it in practice.
To declare variables inside the module, we can simply put definition inside the module endmodule
.
module ex1 (input logic clk,
input logic rst_n,
input logic[7:0] in,
output logic[7:0] out);
logic [7:0] value;
endmodule
In the example code we declare 8-bit value value
. Notice that it is highly recommended to declare the variable type before using the variable. Although implicit logic declaration is supported in SystemVerilog, it is dangerous and usually triggers compiler warnings/errors.
3.1.1 Module Parameters
SystemVerilog allows the module definition parametrized by certain values, which makes the modules more reusable. For instance, suppose we have an ALU module parametrized by the data width, we can reuse the same definition for both 32-bit and 64-bit ALU instantiation.
To declare a parametrized module, we can use the following syntax, which is also the ANSI style.
module mod_param
#(parameter int WIDTH=8,
parameter logic[7:0] VALUE='h0)
(input logic clk,
input logic rst_n,
input logic[WIDTH-1:0] in,
output logic[WIDTH-1:0] out);
endmodule
In the example above, module mod_param
is parametrized by two parameters WIDTH
and VALUE
. We immediately use WIDTH
to parametrize the bit-width of in
and out
. Notice that we also specify the data type for VALUE
. In general we recommend to specify the data type of a parameter. If it used for data width parametrization, int
should suffice. In the example we also give the parameters a default value, which is highly recommended to do so.
There is another type of “parameter” called localparam
. It is not parameterization per se, since its value cannot be changed through instantiation. However, for the sake of completeness we will cover it here. Local parameters are typically used for storing magic numbers. For state values, however, you should use enum
instead.
In the example above we define a magic number VALUE
to have the value of 42. We can later use VALUE
whenever we need its value.
3.1.2 Module Instantiation and Hierarchy
Once we have a module definition, we can instantiate it in a parent module. Suppose we have a module definition as follows:
We can instantiate the child module as follows:
module parent;
// declare variables to connect to the child
logic clk;
logic in;
logic out;
// instantiate a child
child child_inst (
.clk(clk),
.in(in),
.out(out)
);
endmodule
In the example above, we first declares three variables, clk
, in
, out
, which will be wired to our child instance. To instantiate the child module, we create an instance called child_inst
. To specify the port wiring, we use .child_port_name(parent_var_name)
syntax. It means to wire parent_var_name
from the parent module to child_port_name
port from the child instance.
There is another short-hand to instantiate the child module in our case. Since the child_port_name
is identical to parent_var_name
, we can do the following
(.*)
tells the compiler to automatically find and wire matching variable from the parent module. You can even override the default matching with extra connections, as shown below, which wires clk_in
to child_inst
’s clk
and leaves the rest to the default matching.
Although it may simplify the code and make it more readable, because the matching only relies on the name, it may be matched to an unexpected wire. We recommend to only use this style when the design is simple.
To instantiate a module with different parameter values other than the default ones, we can do the following, using the module mod_param
defined earlier.
module parent;
logic clk;
logic rst_n;
logic [15:0] in;
logic [15:0] out;
mod_param #(.WIDTH(16)) child_inst (
.clk(clk),
.rst_n(rst_n),
.in(in),
.out(out)
);
endmodule
In the example above we override the parameter value WIDTH
with 16. Notice that we have to manually change the bit-width of in
and out
. A better way to do is the following, where the bit-width is only specified by a single parameter in the parent scope.:
module parent;
localparam int WIDTH = 16;
logic clk;
logic rst_n;
logic [WIDTH-1:0] in;
logic [WIDTH-1:0] out;
mod_param #(.WIDTH(WIDTH)) child_inst (
.clk(clk),
.rst_n(rst_n),
.in(in),
.out(out)
);
endmodule
To access variables through hierarchy, we can do child_inst.out
from the parent module. We only recommend to do so in test bench, in instead of RTL code for synthesis.
A design style where all the logic are specified through module instantiation rather than the procedural blocks is called structural Verilog. Unless you are very experienced in RTL design or have a particular need in physical design, we highly recommend not to use such style in RTL design. It will reduce the synthesis quality and make verification more difficult. We will discuss the benefit of another style, behavioral Verilog, where design logics are specified through procedural blocks.
3.2 Continuous Assignment
Continuous assignment wires the values on the right hand side to the left side. Continuous in its name implies that whenever an operand in the right-hand expression changes, the whole right-hand side expression shall be evaluated and its result will be assigned to the left hand side. This is used to model combinational circuit where the output of the circuit updates its value whenever the input values change.
To use continuous assignment, we can do
You can of course use more complex expression such as
There are couple rules apply to continuous assignments:
- Continuous assignment can only appear in the scope of a module, that is, its lexical parent should be
module
. You cannot declare a continuous assignment in other scopes such as procedural blocks or functions, which we will cover shortly. Each bit of left hand side can only be assigned to once as continuous assignments. For instance, it is illegal to do something below, where bit
a[1]
is assigned twice.The left hand can only be a net/variable, or a select of a vector/net, or a concatenation. For the case of concatenation, the operator can be seen as “unpacked” in the concatenation order, as shown below, where the sum of
a
,b
, andcin
is split intocout
andsum
. Sincecout
is only 1-bit, it gets the value of carry out. ```SystemVerilog // a 4-bit adder with carry logic [3:0] a; logic [3:0] b; logic [3:0] sum; logic cin; logic cout;assign {cout, sum} = a + b + cin;
Notice you can also perform a continuous assignment when declaring a variable as initialization, as below:
SystemVerilog logic [3:0] d = 4’h1;Although it works well for ASIC with constant initialization, it will only work with a subset of FPGA boards and you shall check the targeted compiler when using this syntax. We recommend not to use this syntax if the code is intended to be portable. However, the following syntax:
SystemVerilog logic a = b & c; ``` may not be synthesizable for some synthesis tools. We highly recommend to use continuous assignment for this use case.
3.3 Procedural Blocks
Procedural blocks, also known as processes, are the building blocks for a module’s logic. There are five different procedural blocks:
- initial procedure
- always procedure
always
always_comb
always_latch
always_ff
- final procedure
- Task
- Function
We will cover each procedural blocks in details in a slightly different order. We will not cover the Verilog-95 procedural block always
here since it is out-dated and error-prone compared to the new syntax.
3.3.1 always_comb
: Modeling Combination Logic
The keyword always_comb
denotes the combinational nature of the procedure block: every logic contained inside the block will be synthesized into combinational circuits. The general syntax for always_comb
is shown below:
begin
and end
are needed if there are more than one statements in the block. If there is only one statement, we can drop the begin
and end
, e.g.
It is up to the design style in your project whether such syntax is allowed. In this bool we will use begin
and end
regardless of the number of statements inside the block.
There are several rules applies to always_comb
:
- Similar to
assign
, the bits on the left hand side can only assigned in a singlealways_comb
. Some simulator may not error out when there is multiplealways_comb
blocking assigning to the same bit, but that is undefined behavior. You cannot mix the bit assignment with other procedural blocks either. - The evaluation of each statement is in-order. The simulator will go through each statement from top to bottom and evaluate them.
The simulator will re-evaluate the block whenever a variable on the right-hand side changes. However, there are several exceptions. One major exception is that there is no “self-triggering”. When the variable both exists on the left hand and right hand side, updating that variable will not trigger re-evaluation, as shown below:
When the value ofb
changes, the always procedure will only be evaluated once.- One benefit of using
always_comb
is that it forces synthesis tool to check your code based on the design intention. If any variable insidealways_comb
is inferred as a latch, the tool shall issue a warning or error. We will discuss under which condition latch inference happens when we introduce conditional control constructs. always_comb
is also sensitive to the contents of a function, which we will cover shortly.In simulator, the simulator will evaluate the
always_comb
once after theinitial
andalways
procedures have been started.
3.3.2 always_latch
: Modeling Latched Combinational Logic
The always_latch
construct functionally is identical to always_comb
except for the fact that it tells the synthesis tools to check whether enclosed logic presents latched logic. All the other rules applied to always_comb
are applicable to always_latch
.
3.3.3 always_ff
: Modeling Sequential Logic
The syntax for always_ff
is shown below:
The signal list inside @()
is called sensitivity list, which tells the synthesis tools that the signal updates are triggered by the sensitivity list. Keyword posedge
implies that the procedure shall be evaluated at the positive (rising) edge of the signal, and negedge
implies the negative (falling) edge of the signal. All the signals in the sensitivity list should be 1-bit.
For RTL design, there are generally two different ways to implement a reset, i.e. synchronous reset and asynchronous reset. The are mainly distinguished by whether to include reset signal in the always_ff
sensitivity list. If reset signal is included, then it is asynchronous reset, meaning the reset can happen independently of the clock. In ASIC design, there are advantage and disadvantages of using asynchronous reset:
- Asynchronous reset can result in cleaner data path, if the technology library has async-reset flip-flops, which is typically the case. This implies that we can push the limit for data-path timing.
- Because of the additional triggering of reset signal, asynchronous reset results in slightly bigger circuits. However, in a modern ASIC design where there are billion gates, adding one or two gates to each register is not a big problem.
- For asynchronous design, if the assertion/de-assertion of reset signal is close to the clock edge, the circuit will go to a metastable state, and as a result the reset state could be lost.
Whether to use synchronous or asynchronous reset depends on your design needs and style guide, as long it is used consistently. In this book we will use asynchronous reset whenever necessary.
Another aspect of the reset is posedge/negedge reset. If negedge
is used in the sensitivity list, it is said to reset low, and reset high
for posedge
. Due to some legacy reasons, modern ASIC technology only offer registers with reset low. As a result, if the design uses posedge reset, an inverter gate will be used with the standard cell. Again, adding one gate for each register is not that much an issue when modern ASIC designs. Whether to use reset high or low depends on your style guide. In this book we will use reset low.
Notice that due to naming convention, if the reset is reset low, we usually suffix _n
at the end of the signal name to signify that it is negedge reset, e.g., rst_n
, reset_n
. In this book we will follow this convention.
In additional to the sensitivity list, always_ff
also uses a special assignment called nonblocking assignment. Contract to normal assignment, called blocking assignment where =
is used, nonblocking assignment uses <=
. All the assignments in always_ff
should be nonblocking assignment, and nonblocking assignment can only be used inside always_ff
, for synthesis purpose. Although mixing blocking and nonblocking assignments is allowed in test bench code, it is strongly discouraged.
The simulation semantics for nonblocking assignment is also different from blocking assignment. As the name suggests, the value update is not “blocking”, that is, the left hand side is not updated immediately, as shown in the example below.
logic a, b;
// assume a = 0, b = 1 before the evaluation
always_ff @(posedge clk) begin
a <= b;
b <= a;
end
// after the evaluation, a = 1, b = 0.
logic a, b;
// assume a = 0, b = 1 before the evaluation
always_comb begin
a = b;
b = a;
end
// after the evaluation, a = 1, b = 1
In the always_ff
block, when the simulator evaluate the first assignment a <= b
, it will evaluate the right hand side first, store the result value internally, and then proceed to the next statement. After every statement is evaluated, the simulator will update the left hand side at the same time. Hence a
will get b
’s value before the clock edge and b
gets a
’s.
In the always_comb
block, however, the simulator will update the left hand side immediately after evaluating the right hand side, before going to the next statement, hence blocking. In this case, after the first assignment, both a
and b
will be 1.
This nonblocking assignment simulation semantic is designed to mimic the actual physical behavior. In the physical circuit, as long as there is no timing violation, at the clock edge, the flip-flop will take whatever values on its input wires and do an update. It does not care about whats the immediate value between the clock edges. If you wire two flip-flops in a loop, as shown in the example, at the clock edge, the flip-flop can only grab each other’s old value, since the update has not happened yet.
This semantics also allows priority coding in always_comb
, as shown below:
Since it is blocking assignment, although after the first statement, a
becomes 0, after the second assignment, a
is re-assigned to b
. This kind of coding style is perfectly legal and sometimes preferred, as we will discuss in the book.
However, if you do that in always_ff
with non-blocking assignment, the result is undetermined. Different simulators and synthesis tools may have different interpretation and you may see inconsistent simulation and synthesis result. This kind of usage should be prohibited.
Similar to other always blocks, variable can only be assigned inside the same always_ff
block.
3.3.4 initial
Procedure
An initial
procedure will execute when the simulator starts and will only execute once. In ASIC design, initial
procedure is not synthesizable and will be ignored during synthesis - most synthesis tools will report a warning.
The most common way to use initial
procedure is for test bench, where stimulus are provided in initial
procedure to drive the simulation. We will discuss more in details when we discuss test bench design.
An example of initial
is provided below:
3.3.5 final
Procedure
Similar to initial
procedure, final
will be executed at the end of simulation and will only be executed once. If there are multiple final
procedures, they will be executed in arbitrary order. final
procedures are usually used for display simulation statistics or cleaning up the simulation environment.
3.3.6 Functions
Similar to C/C++, functions in SystemVerilog allows designers to reuse useful logic. The syntax for function is shown below:
function void void_function(logic a, logic b);
// statements
endfunction
function logic function_with_return_type(logic a, logic b);
// statements
// e.g. return a + b;
endfunction
For functions that has return type, keyword return
must to be used to indicate return value. In old Verilog-95, return value can be assigned via function_name = return_value;
. This style is outdated and we will use keyword return
instead.
There is another style of writing functions that allows multiple outputs:
function void multiple_outputs(input logic a, output logic b, output logic c);
b = a;
c = ~a;
endfunction
In the example above, logic
b
and c
will be assigned after the function call. This is similar to reference arguments in C++.
If your function is recursive, keyword automatic
is needed so that the tools will allocate separate stack space when simulate. We will discuss the reasoning when we introduce the variable scoping rules.
function automatic void auto_func(logic[1:0] a);
// statements
// e.g. auto_func(a -1); as recursive calls
endfunction
Functions in SystemVerilog is synthesizable with certain restrictions:
- Functions cannot have any timing controls statements, such as details and semaphore, or any calls to constructs that have timing controls statements, such as tasks.
- Recursive functions must be able to fully elaborate during synthesis. Synthesis tools typically inline function and unroll the recursion. Undetermined recursion does not guarantee a finite and fixed number of recursion, thus cannot be realized into hardware. This is similar to recursive template in C++, where the template expansion happens during compilation.
To call the function, there are general two ways:
function logic and_func(logic in1, logic in2);
return in1 & in2;
endfunction
logic a, b, c;
always_comb begin
// style 1
c = and_func(a, b);
// style 2
c = and_func(
.in1(a),
.in2(b));
end
Style 1 is similar to function calls in other software programming languages and style 2 is similar to module instantiation in SystemVerilog. In general, if the function only has a few arguments and does not use input
/output
in their function signature, we will use style 1, and style 2 otherwise.
If the return value of a function call is not needed, most compilers will issue a warning or error. We need to cast the return value to void
to avoid this issue:
3.3.7 Tasks
Tasks are very similar to function
except the following things:
- Tasks allow timing controls in their enclosed logic. The timing control can be delay, fork, and other statements.
- Tasks do not have a return type.
Although some synthesis tools might be able to synthesize tasks that do not have timing control statements, we highly recommend you to use functions for RTL design, and tasks for simulation and verification.
The general syntax for task is shown below:
3.4 Procedural Statements
Procedural statements, as the name suggests, can only exist inside the procedural blocks such as always_comb
and function
. There are many types of procedural statements and we will cover the following types:
- Selection statement:
if
andcase
statement - Loop statements:
for
andwhile
- Jump statements:
continue
, andbreak
We will cover loop and jump statement together since they are often used together.
3.4.1 if
Statement
The syntax for if
statement is shown below
// one statement body
if (expr)
[statement 1];
// multiple statement body
if (expr) begin
[statement 1];
[statement 2];
end
// with else
if (expr) begin
[statement 1];
end
else begin
[statement 2];
end
// else if conditions
if (expr1) begin
[statement 1];
end
else if (expr2) begin
[statement 2];
end
else begin
[statement 3];
end
Whether to omit begin ... end
when there is only one statement depends on the style guide. In this book we will omit begin ... end
whenever it makes code easier to read.
Although expr
can actually be a multi-bit expression, since the condition is evaluated against zero, it is generally suggested to make it 1-bit. For instance,
should be written as
for clarity. Like C/C++, dangling else
can also be a problem when begin ... end
is omitted for nested if statement. In this case we suggest you always use begin ... end
block.
3.4.1.1 Additional Keywords for if
Statement
SystemVerilog offers several keywords to if
statement can be useful to check the correctness of implementation:
unique
. An error will be issued during simulation if no condition matches unless there is anelse
statement. For instance// logic [1:0] a; unique if (a == 0) $display("a is 0); else if (a == 1) $display("a is 1); // this results in an error since a == 2 and a == 3 is not covered in conditions.
However, if
unique0
is used, there will be any violation report. In general, useunique0
whenever there are some cases not covered by the conditions, but serve no logic.unique
andunique0
are used to guarantee that there is no overlap betweenif
conditions. This allows the synthesis tools to optimize the multiplexing logic. Notice that there is a inherent priority logic inif
statement. If condition 1 is lexically before condition 2, condition 1 will be checked first, then condition 2. Addingunique
/unique0
allows the condition checking to be parallel, thus producing more performant circuit.priority
explicitly tells tools that if there is overlaps in the conditions, use the lexically precedent condition first. This is useful to prevent inconsistent behavior between simulators and synthesis tool, where the simulator by default checks conditions in order whereas the synthesis may compile a parallel circuit due to synthesis macros or commands. This ensures the designer’s intent get passes to various tools in a consistent manner.
These keywords are introduced to remove inconsistency between simulator and synthesis tools. Unless explicitly specified in the design style guide, we recommend use these keywords as much as possible. However, in some cases, this keywords may increase the workload for formal verification tools, thus prohibited in some design companies. Again, these keywords usage depends on your project specific design style guide.
3.4.1.2 Latch Created from if
Statement
A latch is created if the logic’s value depends on its previous value. If not specified properly, variables used inside if
statement will be inferred as a latch during synthesis, resulting in undesired behavior. In the example below, we create a latch unintentionally:
If a = 1
, we will have b = 1
. However, if a = 0
, b
will not be set, thus retaining its old value. As a result, we have created latch a
. Notice that since we are using always_comb
keyword, synthesis will report either a warning or error once a latch is inferred. There are usually two ways to solve latch issue:
- Specify a default value at the beginning of the
always_comb
block: ```SystemVerilog always_comb begin b = 0; if (a) b = 1; end - Fully specify the
if
conditions:SystemVerilog always_comb begin if (a) b = 1; else b = 0; end
Choosing which one to use depends on the logic. Sometimes setting default value makes code simpler and sometimes fully specified if
statement makes the code more readable. It is up to designer to choose how to avoid latch.
In additional to always_comb
, you can also explicitly create a latch inside always_ff
, especially asynchronous reset is used:
logic clk, rst_n, a, b
always_ff @(posedge clk, negedge rst_n) begin
if (!rst_n) begin
a = 0;
end
end
In the example above we are missing the condition where rst_n
is high. Since a
’s value change only depends on rst_n
(asynchronous reset), a
actually does not depends on the clock edge. Hence the synthesis tool will infer a latch, instead of a flip-flop. Then again, since we use always_ff
, an error/warning will be issued from synthesis tools.
To avoid creating latch, besides being careful when writing the logic, we can also resort to commercial SystemVerilog linters or even an elaboration analysis from synthesis tools. We will not cover linter in this book.
3.4.1.3 if
Statement with Reset Logic
Although allowed by the language specification, stacking two if
statements in the always_ff
is not allowed in some synthesis tools, such as Design Compiler®:
logic clk, rst_n, a, b, in;
always_ff @(posedge clk, negedge rst_n) begin
if (!rst_n) begin
a <= 0;
end
else begin
a <= 0;
end
if (!rst_n) begin
b <= 0;
end
else begin
b <= 0;
end
end
The code above will trigger ELAB-302
error since it contains two if
statements in the always_ff
block. There are two solutions for that:
- If
a
andb
are totally separate, use twoalways_ff
instead. - If
a
andb
are related, e.g. sharing the same input conditions, merge these twoif
statements into oneif
statement.
3.4.2 case
Statement
case
statement is similar to switch
statement in C/C++ with some semantic difference due to the nature of hardware design. The general syntax for case
is shown below:
// logic [1:0] a;
case (a)
2'b00: begin
// statements
end
2'b01:
// one single statement
default: begin
// statements
end
endcase
Notice that we do not have break
statement inside each case
condition clause, which is the major difference compared to that of C/C++. As a result, there is no switch fall through in SystemVerilog. To take into the intentional fall through use case (shown in C++ below), we can put multiple conditions in the same case statement.
If there is only one statement for a particular case condition, we can omit begin...end
. However, if any conditions has more than one statement, we recommend to use begin...end
to enclose all conditions for readability.
SystemVerilog also allows to use range and wildcards as conditions using inside
keyword, which is shown below:
// logic [3:0] a;
case (a) inside
4'b0???: begin
// statements
end
[8:12]: begin
// statements
end
default: begin
// statements
end
endcase
In the example, ?
is regarded as don’t care, which means it will match with any 4-state bit. For instance, if a = 4'b0xxxx
, it will match with the first case. [8:12]
is a range construct that is lower and upper bound inclusive. For instance, if a = 4'b1001
, it will match with the second case. If nothing matches, it will go to the default
condition.
Like if
statement, we can add modifiers to case
statement. The most commonly used is unique
. Keep in mind that the default case
statement has priority. That is, if two conditions overlap, the execution will follow the first condition lexically. The synthesis tools are required to obey this convention as well. To produce optimal circuit, physical design engineers often use synthesis directives to remove such priority. However, removing priority creates an inconsistency between the simulator and synthesis tools, resulting in potential bug that can only be caught during gate-level simulation. Using unique
forces the simulator to check if there is any conditions overlapping, which guarantee the consistency among tools. However, although using unique
is highly recommended whenever possible, in some large designs, we may see exponential growth in runtime with some tools, e.g. Formality® from Synopsys®.
3.4.3 Loop Statements
Like C/C++, SystemVerilog also offers for
and while
loop for control logic. However, since the synthesis tools need to compile the logic into logical gates that compute in finite and deterministic cycles, the loop-bound has to be known during compile time; otherwise the tool either reports an error, or generated unwanted logic.
The general syntax for for
loop is shown below:
Here we use i
as int
since it doesn’t matter whether i
is 2-state for 4-state values. In situation where index i
is used in arithmetics with 4-logic values, we need to declare i
as 4-state variable. Notice that the loop upper bound 42
is a static value known during compilation time. If we use a variable as upper bound, a latch will be used since the synthesis tool assumes the upper bound could be 0, in such case the value update follows the latch inferring rules. However, we can disregard such rules when using for loops in test bench code, since the simulator is less picky.
The general syntax for while
loop is shown below:
Unlike for
loop, most synthesis tools cannot take while
loop construct, since that requires compile-time full elaboration of the loop body, which can be tricky to do. We can convert the loop body into a for
loop if needed. Again, there is no such restriction in the test bench code.
There is a variant of while
loop in SystemVerilog, i.e. do...while
, which has similar semantics as C/C++. The syntax for do...while
loop is
To exit the loop body early, we can use the break
statement, similar to C/C++. Some synthesis tools will optimize the circuit when it notices the break
statement.
3.5 Generate statements
SystemVerilog allows users to “dynamically” create circuit logic using generate construct. Users can use for-loops or conditional generation to meta-program the circuit. However, such meta-programming has limitations:
- Generate is evaluated during elaboration time. Hence we cannot add or remove circuit during runtime.
- We cannot create ports using generate; net, such as
logic
, is allowed.
3.5.1 Loop generate
The syntax for loop generate is shown below:
generate
for (genvar i = 0; i < 4; i++) begin
// statements
// e.g.
assign a[i] = b[i];
// or even instantiations
module_name inst (
.a(a[i]),
.b(b[i])
);
end
endgenerate
Notice that unlike normal for
loop, we need to declare the loop variable i
using keyword genvar
. However, genvar
is used as an integer during elaboration to evaluate generated constructs. The loop bounds has to be known during compilation elaboration time, otherwise an error will be thrown. Loop generate can also be nested together to create more complex circuit logics.
When creating instance using generate statement, the hierarchy name is slightly different. We will cover the naming convention in next section.
3.5.2 Conditional generate
In many cases we need to conditionally generate logic based on the parametrization. SystemVerilog supports conditional generate constructs, as long as they can be elaborated statically during compilation time. The “conditional” part is typically done via if
or case
statements where the condition expression value is known. For instance, we can do the following conditional generate:
module A;
endmodule
module B;
endmodule
module C;
endmodule
module GenMod #(parameter int value = 0);
// use if statement to conditional generate instances
generate
if (value == 0) begin
A a();
end
else begin
B b();
end
endgenerate
// use case statement to conditional generate
generate
case (value)
0: begin
A a();
end
1: begin
B b();
end
default: begin
C c();
end
endcase
endgenerate
endmodule
In the example we create instances based on the value of the parameter value
. When module GenMod
is instantiated with different parameter values, we will create instances accordingly. We can also create variables and connecting them inside the generate statement. For instance
3.6 Named Blocks, Scope Rules, and Hierarchical Names
SystemVerilog offers couple language features that allow programmers debug their hardware design easier than before. One demand for reliable debugging is the ability to address every signal by name hierarchically. With that ability, we can add assertions or verification tasks to the signals of interest without changing the design. This feature is achieved by allowing named blocks and defining scope rules.
3.6.1 Named Blocks
In SystemVerilog, you can name any code blocks defined by begin ... end
, which can be used later on for hierarchical data access. The general syntax for naming code blocks is shown below:
In the example above, we created two named blocks with the name name_1
and name_2
respectively. Notice that people also add the names next to the end
keyword so that it would be symmetric to the name used next to begin
, which is, again, a form of coding style.
Notice that there are several cases where named blocks are highly recommend since it helps you to debug the code:
- The code blocks are generated through
generate
construct. By language specification, if the generated code block is not named, it will obtaingenblk{NUM}
as its identifier, where{NNUM}
is substituted with the index of generated code blocks in the module scope. - In cases such as functions or
always_comb
where you declare a temporary variable inside the scope, it is always a good practice to name the block so that you can refer to it later when debugging with the waveform, or setting up assertions. - Using labels will also help to reader to identify the scope. This particular helpful when the number of lines in the code block is more than 20 lines. This is particular true for functions and modules.
From now on we will use named block whenever appropriate.
3.6.2 Scope Rules
Similar to software programming languages, SystemVerilog has a set of scoping rules. The following constructs define a new scope:
- Modules
- Interfaces
- Programs
- Checkers
- Packages
- Classes
- Tasks
- Functions
- begin-end blocks (named or unnamed)
- fork-join blocks (named or unnamed)
- Generate blocks
We have covered most of the constructs so far and will cover the reset of it later in the book.
In general, variables created in outer scope can be accessed in an inner scope, and illegal access the other way around. Two scopes at the same levels are isolated as well. Although it is somewhat unrelated to the scope rules, variable declaration inside a begin-end blocks need to follow ANSI-C style, that is, variable declaration has to be at the beginning at the scope. Declaration with assignment counts as normal statement, hence is illegal in the middle of statements, as shown below:
logic c, d;
always_comb begin
// this is legal
logic a, b;
a = !c;
b = c;
d = a ^ b;
end
always_comb begin
logic a;
a = !c;
// this is illegal
logic b = c;
d = a ^ b;
end
3.6.3 Hierarchical Names
In SystemVerilog, every identifier has a unique hierarchical path name. To do so, we can use “dot-notation” where .
is used to access the child scope. There are some rules when resolving hierarchical names:
- You can always access child scope variables.
- If the parent scope is visible to the child scope, the child scope can use parent scope’s identify to access other scopes/variables.
Here is an example of accessing hierarchical names:
module m;
logic a; // var1
logic b; // var2
always_comb begin: A
logic c; // var3
// this is illegal since top is not visible in this scope
// top.d = 0;
end
endmodule: m
module top;
// module instantiation
m m1();
m m2();
logic d; // var4
initial begin: B
logic a; // var5
B.a = 0; // accessing var5
m1.a = 0; // accessing m1's var1
m1.b = 0; // accessing m1's var2
m2.A.c = 0; // accessing m2's var3
// this is also legal since top scope is visible here
top.m2.b = 0;
end
endmodule: top
In general all the identifiers are public in a scope, meaning you are always be able to access it hierarchically. One exception is class variables declared as local
, which will be covered later in the book.
3.6.4 Lifetime
Variables declared inside a module and interface have a static life lifetime by default. That means that all the variable will be instantiated/mapped at the beginning of simulation time, and will remain the same through out the entire simulation time. Variables declared inside a function or task by default are static. That means if a function is called multiple times without finishing, e.g. recursive calls or called through fork
, the static variables will be overridden unexpectedly. To solve this issue, we can either declare the function/task automatic
, e.g. function automatic foo()
, or declare the variable automatic, e.g. automatic logic var
. All variables declared inside an automatic
function/task is by default non-static, as their lifetime is set to the life-time of the parent scope, which is similar to local variable declared inside a function in C/C++. Notice that automatic
works with synthesizable code as well, even though the semantics are defined in terms of simulation environment.
3.7 Interface
Interface is a new construct introduced by SystemVerilog to encapsulate the reusable communication between different entities such as design and verification blocks. The concept of “interface” is similar to that of software programming language such as C#. However, instead of providing public accessible functions, interface in SystemVerilog defines a bundle of port names, connections, and functions associated with the ports.
Interface can be used as ports or internal wires that connects instances. Below is an example of configuration bus interface and its instantiation and usage. Notice that if the interface does not have any input/output ports, we need to instantiate it with ()
. Module config_reg
takes a “generic” type port using the keyword interface
. As a result, the module can take any be instantiated with any interface wires as long as it has required definitions. In other words, the config bus is abstracted away at the config_reg
level, a concept similar to object-oriented programming’s generic concept. The legality will be check at compile time as usual.
interface config_bus;
logic clk;
logic rst_n;
logic[15:0] config_addr;
logic[15:0] config_data;
logic config_en;
endinterface
module config_reg #(
parameter logic[15:0] CONFIG_ADDR = 16'h0
) (
interface i
);
logic [15:0] config_reg_0;
always_ff @(posedge i.clk) begin
if (!i.rst_n) begin
config_reg_0 <= 0;
end
else if (i.config_en && i.config_addr == CONFIG_ADDR) begin
config_reg_0 <= i.config_data;
end
end
endmodule
module top;
config_bus bus();
config_reg #(.CONFIG_ADDR(0)) reg0 (bus);
config_reg #(.CONFIG_ADDR(1)) reg1 (bus);
endmodule
You can also explicitly specify the interface type in the module definition, as shown below:
Below is an example of interface that has input and output ports. Notice that the instantiate is similar to that of module.
interface config_bus (
input clk,
input rst_n
);
logic[15:0] config_addr;
logic[15:0] config_data;
logic config_en;
endinterface
// definition for config_reg remains the same
module top;
logic clk, rst_n;
config_bus bus(.clk(clk), .rst_n(rst_n));
config_reg #(.CONFIG_ADDR(0)) reg0 (bus);
config_reg #(.CONFIG_ADDR(1)) reg1 (bus);
endmodule
SystemVerilog also allows using keyword modport
to create a “new interface” within the interface by specifying the directions of wires within the scope of the interface, as shown in the example below.
interface data_bus;
logic clk;
logic rst_n;
logic m_valid;
logic m_ready;
logic[7:0] m_data;
logic s_ready;
logic s_valid;
modport master (
input clk,
input rst_n,
output m_valid,
output m_ready,
output m_data,
input s_ready,
input s_valid
);
modport slave (
input clk,
input rst_n,
input m_valid,
input m_ready,
input m_data,
output s_ready,
output s_valid
);
endinterface
module master (interface i);
// master logic
endmodule
module slave (interface i);
// slave logic
endmodule
module top;
data_bus bus();
master master(bus.master);
slave slave(bus.slave);
endmodule
Because modport
allows us to specify wire direction, the compiler can check the connections and make sure there is no multiple drivers on the same wire. By default, any wire declared inside the interface as inout
connectivity. As a result, some synthesis tools may give warnings even though the connections is correct, hence modport
is recommended. Since the wires are all connected together, we don’t need to explicit wire the connection inside the interface, i.e. master.clk
is the same signal as slave.clk
. modport
construct can be used as any other interface when connected to module instances. Again, you can also specify the interface type in the module definition as below:
Like module definitions, interface
in SystemVerilog also supports parametrization. The syntax is identical to that of modules, as shown below.
interface config_bus #(
parameter int ADDR_WIDTH=16,
parameter int DATA_WIDTH=16
);
logic clk;
logic rst_n;
logic[ADDR_WIDTH-1:0] config_addr;
logic[DATA_WIDTH-1:0] config_data;
logic config_en;
endinterface
// to instantiate it
module top;
config_bus #(.ADDR_WIDTH(8), .DATA_WIDTH(16)) bus;
endmodule
You can also add functions and tasks to the interface. Notice that for synthesizable RTL code we are limited to functions or tasks without any timing control logic. In addition, the function also has to be declared as automatic
. Below is an example about interface with functions.
interface config_bus #(
parameter int ADDR_WIDTH=16,
parameter int DATA_WIDTH=16
);
logic clk;
logic rst_n;
logic[ADDR_WIDTH-1:0] config_addr;
logic[DATA_WIDTH-1:0] config_data;
logic config_en;
function automatic logic config_activate(logic[ADDR_WIDTH-1:0] addr);
return config_en && (config_addr == addr);
endfunction
endinterface
module config_reg #(
parameter int ADDR_WIDTH=16,
parameter int DATA_WIDTH=16,
parameter logic[ADDR_WIDTH-1:0] CONFIG_ADDR = 16'h0
) (
interface bus
);
logic[ADDR_WIDTH-1:0] config_data;
always_ff @(posedge bus.clk, negedge bus.rst_n) begin
if (!bus.rst_n) begin
config_data <= 16'h0;
end
else if (bus.config_activate(CONFIG_ADDR)) begin
config_data <= bus.config_data;
end
end
endmodule