赞
踩
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.
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.
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.
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:
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.
- assign a[1:0] = b[1:0];
- assign a[1] = c;
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.
Procedural blocks, also known as processes, are the building blocks for a module’s logic. There are five different procedural blocks:
always
always_comb
always_latch
always_ff
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.
always_comb
: Modeling Combination LogicThe 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
:
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.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.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.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 the initial
and always
procedures have been started.
always_latch
: Modeling Latched Combinational LogicThe 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
.
always_ff
: Modeling Sequential LogicThe 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:
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.
initial
ProcedureAn 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
final
ProcedureSimilar 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.
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:
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:
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
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:
if
and case
statementfor
and while
continue
, and break
We will cover loop and jump statement together since they are often used together.
if
StatementThe 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.
if
StatementSystemVerilog 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 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.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.
if
StatementA 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:
always_comb
block: ```SystemVerilog always_comb begin b = 0; if (a) b = 1; endif
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.
if
Statement with Reset LogicAlthough 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:
a
and b
are totally separate, use two always_ff
instead.a
and b
are related, e.g. sharing the same input conditions, merge these two if
statements into one if
statement.case
Statementcase
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®.
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.
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:
logic
, is allowed.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.
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
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.
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:
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.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.From now on we will use named block whenever appropriate.
Similar to software programming languages, SystemVerilog has a set of scoping rules. The following constructs define a new scope:
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
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:
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.
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:
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_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:
- 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 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[ADDR_WIDTH-1:0] config_addr;
logic[DATA_WIDTH-1:0] config_data;
function automatic logic config_activate(logic[ADDR_WIDTH-1:0] addr);
return config_en && (config_addr == addr);
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
end else if (bus.config_activate(CONFIG_ADDR)) begin
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。