# 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:

module module_name;
// content of the module
endmodule

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.

module module_name (input  logic      clk, rst_n,
input  logic[7:0] in,
output logic[7:0] out);
endmodule

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.

module localparam_ex;

localparam logic[31:0] VALUE = 32'd42;

endmodule

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:

module child (input  logic clk,
input  logic in,
output logic out);

endmodule

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

child child_inst (.*);

(.*) 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.

logic clk_in;

child child_inst (.clk(clk_in), .*);

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

module continuous_assign;
logic [3:0] a;
logic [3:0] b;

assign a = b;
endmodule

You can of course use more complex expression such as

logic c;
assign a = c? b : b + 1;

There are couple rules apply to continuous assignments:

1. 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.
2. 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.

assign a[1:0] = b[1:0];
assign a[1] = c;
3. 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, and cin is split into cout and sum. Since cout 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
• 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.1always_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:

logic a, b, c, d;

always_comb begin
a = b & c;
d = a | b;
end

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.

always_comb
a = b & c;

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:

1. Similar to assign, the bits on the left hand side can only assigned in a single always_comb. Some simulator may not error out when there is multiple always_comb blocking assigning to the same bit, but that is undefined behavior. You cannot mix the bit assignment with other procedural blocks either.
2. The evaluation of each statement is in-order. The simulator will go through each statement from top to bottom and evaluate them.
3. 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:

 logic a, b, c;
always_comb begin
c = a ^ b;
a = b & c;
end
When the value of b changes, the always procedure will only be evaluated once.
4. One benefit of using always_comb is that it forces synthesis tool to check your code based on the design intention. If any variable inside always_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.
5. always_comb is also sensitive to the contents of a function, which we will cover shortly.
6. In simulator, the simulator will evaluate the always_comb once after the initial and always procedures have been started.

### 3.3.2always_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.3always_ff: Modeling Sequential Logic

The syntax for always_ff is shown below:

always_ff @(posedge clk, negedge rst_n) begin
// statements
end

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:

1. 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.
2. 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.
3. 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:


logic a, b;

always_comb begin
a = 0;
a = b;
end
// at the end of evaluation, a = b

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.4initial 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:

logic a

initial begin
a = 0;
end

### 3.3.5final 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:

1. 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.
2. 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:

    void'(and_func(a, b));

Tasks are very similar to function except the following things:

1. Tasks allow timing controls in their enclosed logic. The timing control can be delay, fork, and other statements.
2. 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:

task task_name(input logic a, output logic b);
// statements
endtask

## 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:

1. Selection statement: if and case statement
2. Loop statements: for and while
3. Jump statements: continue, and break

We will cover loop and jump statement together since they are often used together.

### 3.4.1if 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,

logic [3:0] expr;
if (expr) begin
end

should be written as

if (expr == 4'd0) begin
end

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:

1. unique. An error will be issued during simulation if no condition matches unless there is an else 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, use unique0 whenever there are some cases not covered by the conditions, but serve no logic.

unique and unique0 are used to guarantee that there is no overlap between if conditions. This allows the synthesis tools to optimize the multiplexing logic. Notice that there is a inherent priority logic in if statement. If condition 1 is lexically before condition 2, condition 1 will be checked first, then condition 2. Adding unique/unique0 allows the condition checking to be parallel, thus producing more performant circuit.
2. 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:

logic a, b;

always_comb begin
if (a)
b = 1;
end

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:

1. Specify a default value at the beginning of the always_comb block: SystemVerilog always_comb begin b = 0; if (a) b = 1; end
2. 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.3if 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:

1. If a and b are totally separate, use two always_ff instead.
2. If a and b are related, e.g. sharing the same input conditions, merge these two if statements into one if statement.

### 3.4.2case 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.

int a;

switch (a) {
case 0:
case 1:
// statements
break;
default:
// statements
}
// logic [1:0] a;

case (a)
2'b00, 2'b01: begin
// statements
end
default: begin
end
endcase

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:

for (int i = 0; i < 42; i++) begin
// statements
// e.g.
// \$display("index i is : %0d, i);
end

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:


int i = 0;
while (i < 42) begin
// statements
i++;
end

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

int i = 0;
do begin
// statements
i += 1;
end while (i < 42)

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:

1. Generate is evaluated during elaboration time. Hence we cannot add or remove circuit during runtime.
2. 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

generate
if (value == 0) begin
logic v1, v2;
assign v1 = v2;
end
endgenerate

## 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:

always_comb begin: name_1
if (a) begin: name_2
end
end

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:

1. The code blocks are generated through generate construct. By language specification, if the generated code block is not named, it will obtain genblk{NUM} as its identifier, where {NNUM} is substituted with the index of generated code blocks in the module scope.
2. 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.
3. 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
• 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:

1. You can always access child scope variables.
2. 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.

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_data;
logic       config_en;
endinterface

module config_reg #(
) (
interface i
);

logic [15:0] config_reg_0;

always_ff @(posedge i.clk) begin
if (!i.rst_n) begin
config_reg_0 <= 0;
end
config_reg_0 <= i.config_data;
end
end

endmodule

module top;
config_bus bus();

endmodule

You can also explicitly specify the interface type in the module definition, as shown below:

module config_reg // param
(
config_bus i
);
// config_reg logic here
endmodule

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_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));

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[7:0]   m_data;
logic        s_valid;

modport master (
input  clk,
input  rst_n,
output m_valid,
output m_data,
input  s_valid
);

modport slave (
input  clk,
input  rst_n,
input  m_valid,
input  m_data,
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:

module master (data_bus.master i);
// master logic
endmodule

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 DATA_WIDTH=16
);
logic                  clk;
logic                  rst_n;
logic[DATA_WIDTH-1:0]  config_data;
logic                  config_en;
endinterface

// to instantiate it
module top;
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 DATA_WIDTH=16
);
logic                  clk;
logic                  rst_n;
logic[DATA_WIDTH-1:0]  config_data;
logic                  config_en;

endfunction
endinterface

module config_reg #(
parameter int DATA_WIDTH=16,
) (
interface bus
);

endmodule`