function is meant to do some processing on the input and return a single value, whereas a
task is more general and can calculate multiple result values and return them using output and inout type arguments. Tasks can contain simulation time consuming elements such as @, posedge and others.
A task need not have a set of arguments in the port list, in which case it can be kept empty.
// Style 1 task [name]; input [port_list]; inout [port_list]; output [port_list]; begin [statements] end endtask // Style 2 task [name] (input [port_list], inout [port_list], output [port_list]); begin [statements] end endtask // Empty port list task [name] (); begin [statements] end endtask
Often times we find certain pieces of code to be repetitive and called multiple times within the RTL. They mostly do not consume simulation time and might involve complex calculations that need to be done with different data values. In such cases, we can declare a
function and place the repetitive code inside the function and allow it to return the result. This will reduce the amount of lines in the RTL drastically since all you need to do now is to do a function call and pass data on which the computation needs to be performed. In fact, this is very similar to the functions in C.
The purpose of a function is to return a value that is to be used in an expression. A function definition always start with the keyword
function followed by the return type, name and a port list enclosed in parantheses. Verilog knows that a function definition is over when it finds the
endfunction keyword. Note that a function shall have atleast one input declared and the return type will be
void if the function does not return anything.
function [automatic] [return_type] name ([port_list]); [statements] endfunction
automatic will make the function reentrant and items declared within the task are dynamically allocated rather than shared between different invocations of the task. This will be useful for recursive functions and when the same function is executed concurrently by N processes when forked.
module is a block of Verilog code that implements a certain functionality. Modules can be embedded within other modules and a higher level module can communicate with its lower level modules using their input and output ports.
A module should be enclosed within
endmodule keywords. Name of the module should be given right after the
module keyword and an optional list of ports may be declared as well. Note that ports declared in the list of port declarations cannot be redeclared within the body of the module.
module <name> ([port_list]); // Contents of the module endmodule // A module can have an empty portlist module name; // Contents of the module endmodule
All variable declarations, dataflow statements, functions or tasks and lower module instances if any, must be defined within the
endmodule keywords. There can be multiple modules with different names in the same file and can be defined in any order.
The module dff represents a D flip flop which has three input ports d , clk , rstn and one output port q . Contents of the module describe how a D flip flop should behave for different combinations of inputs. Here, input d is always assigned to output q at positive edge of clock if rstn is high because it is an active low reset.
// Module called "dff" has 3 inputs and 1 output port module dff ( input d, input clk, input rstn, output reg q); // Contents of the module always @ (posedge clk) begin if (!rstn) q <= 0; else q <= d; end endmodule
This module will be converted into the following digital circuit during synthesis.
Note that you cannot have any code written outside a module !
What is the purpose of a module ?
A module represents a design unit that implements certain behavioral characteristics and will get converted into a digital circuit during synthesis. Any combination of inputs can be given to the module and it will provide a corresponding output. This allows the same module to be reused to form bigger modules that implement more complex hardware.
For example, the DFF shown above can be chained to form a shift register.
module shift_reg ( input d, input clk, input rstn, output q); wire [2:0] q_net; dff u0 (.d(d), .clk(clk), .rstn(rstn), .q(q_net)); dff u1 (.d(q_net), .clk(clk), .rstn(rstn), .q(q_net)); dff u2 (.d(q_net), .clk(clk), .rstn(rstn), .q(q_net)); dff u3 (.d(q_net), .clk(clk), .rstn(rstn), .q(q)); endmodule
Note that the dff instances are connected together with wires as described by the Verilog RTL module.
Instead of building up from smaller blocks to form bigger design blocks, the reverse can also be done. Consider the breakdown of a simple GPU engine into smaller components such that each can be represented as a module that implements a specific feature. The GPU engine shown below can be divided into five different sub-blocks where each perform a specific functionality. The bus interface unit gets data from outside into the design, which gets processed by another unit to extract instructions. Other units down the line process data provided by previous unit.
Each sub-block can be represented as a
module with a certain set of input and output signals for communication with other modules and each sub-block can be further divided into more finer blocks as required.
What are top-level modules ?
A top-level module is one which contains all other modules. A top-level module is not instantiated within any other module.
For example, design modules are normally instantiated within top level testbench modules so that simulation can be run by providing input stimulus. But, the testbench is not instantiated within any other module because it is a block that encapsulates everything else and hence is the top-level module.
Design Top Level
The design code shown below has a top-level module called design. This is because it contains all other sub-modules requried to make the design complete. The submodules can have more nested sub-modules like mod3 inside mod1 and mod4 inside mod2. Anyhow, all these are included into the top level module when mod1 and mod2 are instantiated. So this makes the design complete and is the top-level module for the design.
//--------------------------------- // Design code //--------------------------------- module mod3 ( [port_list] ); reg c; // Design code endmodule module mod4 ( [port_list] ); wire a; // Design code endmodule module mod1 ( [port_list] ); // This module called "mod1" contains two instances wire y; mod3 mod_inst1 ( ... ); // First instance is of module called "mod3" with name "mod_inst1" mod3 mod_inst2 ( ... ); // Second instance is also of module "mod3" with name "mod_inst2" endmodule module mod2 ( [port_list] ); // This module called "mod2" contains two instances mod4 mod_inst1 ( ... ); // First instance is of module called "mod4" with name "mod_inst1" mod4 mod_inst2 ( ... ); // Second instance is also of module "mod4" with name "mod_inst2" endmodule // Top-level module module design ( [port_list]); // From design perspective, this is the top-level module wire _net; mod1 mod_inst1 ( ... ); // since it contains all other modules and sub-modules mod2 mod_inst2 ( ... ); endmodule
Testbench Top Level
The testbench module contains stimulus to check functionality of the design and is primarily used for functional verification using simulation tools. Hence the design is instantiated and called d0 inside the testbench module. From a simulator perspective, testbench is the top level module.
//----------------------------------------------------------- // Testbench code // From simulation perspective, this is the top-level module // because 'design' is instantiated within this module //----------------------------------------------------------- module testbench; design d0 ( [port_list_connections] ); // Rest of the testbench code endmodule
A hierarchical structure is formed when modules can be instantiated inside one another, and hence the top level module is called the root. Since each lower module instantiations within a given module is required to have different identifier names, there will not be any ambiguity in accessing signals. A hierarchical name is constructed by a list of these identifiers separated by dots
. for each level of the hierarchy. Any signal can be accessed within any module using the hierarchical path to that particular signal.
// Take the example shown above in top level modules design.mod_inst1 // Access to module instance mod_inst1 design.mod_inst1.y // Access signal "y" inside mod_inst1 design.mod_inst2.mod_inst2.a // Access signal "a" within mod4 module testbench.d0._net; // Top level signal _net within design module accessed from testbench
Hardware behavior cannot be implemented without conditional statements and other ways to control the flow of logic. Verilog has a set of control flow blocks and mechanisms to achieve the same.
This conditional statement is used to make a decision about whether certain statements should be executed or not. This is very similar to the
if-else-if statements in C. If the expression evaluates to true, then the first statement will be executed. If the expression evaluates to false and if an
else part exists, the
else part will be executed.
Placing values onto nets and variables are called assignments. There are three basic forms:
Legal LHS values
An assignment has two parts - right-hand side (RHS) and left-hand side (LHS) with an equal symbol (=) or a less than-equal symbol (<=) in between.
|Assignment type||Left-hand side|
The RHS can contain any expression that evaluates to a final value while the LHS indicates a net or a variable to which the value in RHS is being assigned.
module tb; reg clk; wire a, b, c, d, e, f; reg z, y; // clk is on the LHS and the not of clk forms RHS always #10 clk = ~clk; // y is the LHS and the constant 1 is RHS assign y = 1; // f is the LHS, and the expression of a,b,d,e forms the RHS assign f = (a | b) ^ (d & e); always @ (posedge clk) begin // z is the LHS, and the expression of a,b,c,d forms the RHS z <= a + b + c + d; end initial begin // Variable names on the left form LHS while 0 is RHS a <= 0; b <= 0; c <= 0; d <= 0; e <= 0; clk <= 0; end endmodule
Procedural assignments occur within procedures such as always, initial, task and functions and are used to place values onto variables. The variable will hold the value until the next assignment to the same variable.
The value will be placed onto the variable when the simulation executes this statement at some point during simulation time. This can be controlled and modified the way we want by the use of control flow statements such as if-else-if, case statement and looping mechanisms.
reg [7:0] data; integer count; real period; initial begin data = 8'h3e; period = 4.23; count = 0; end always @ (posedge clk) count++;
Variable declaration assignment
An initial value can be placed onto a variable at the time of its declaration as shown next. The assignment does not have a duration and holds the value until the next assignment to the same variable happens. Note that variable declaration assignments to an array are not allowed.
module my_block; reg [31:0] data = 32'hdead_cafe; initial begin #20 data = 32'h1234_5678; // data will have dead_cafe from time 0 to time 20 // At time 20, data will get 12345678 end endmodule
reg [3:0] a = 4'b4; // is equivalent to reg [3:0] a; initial a = 4'b4;
If the variable is initialized during declaration and at time 0 in an initial block as shown below, the order of evaluation is not guaranteed, and hence can have either 8'h05 or 8'hee.
module my_block; reg [7:0] addr = 8'h05; initial addr = 8'hee; endmodule
reg [3:0] array [3:0] = 0; // illegal integer i = 0, j; // declares two integers i,j and i is assigned 0 real r2 = 4.5, r3 = 8; // declares two real numbers r2,r3 and are assigned 4.5, 8 resp. time startTime = 40; // declares time variable with initial value 40
Procedural blocks and assignments will be covered in more detail in a later section.
This is used to assign values onto scalar and vector nets and happens whenever there is a change in the RHS. It provides a way to model combinational logic without specifying an interconnection of gates and makes it easier to drive the net with logical expressions.
// Example model of an AND gate wire a, b, c; assign a = b & c;
Whenever b or c changes its value, then the whole expression in RHS will be evaluated and a will be updated with the new value.
Net declaration assignment
This allows us to place a continuous assignment on the same statement that declares the net. Note that because a net can be declared only once, only one declaration assignment is possible for a net.
wire penable = 1;
Procedural Continuous Assignment
These are procedural statements that allow expressions to be continuously assigned to nets or variables and are of two types.
This will override all procedural assignments to a variable and is deactivated by using the same signal with
deassign. The value of the variable will remain same until the variable gets a new value through a procedural or procedural continuous assignment. The LHS of an
assign statement cannot be a bit-select, part-select or an array reference but can be a variable or a concatenation of variables.
reg q; initial begin assign q = 0; #10 deassign q; end
These are similar to the
assign - deassign statements but can also be applied to nets and variables. The LHS can be a bit-select of a net, part-select of a net, variable or a net but cannot be the reference to an array and bit/part select of a variable. The
force statment will override all other assignments made to the variable until it is released using the
reg o, a, b; initial begin force o = a & b; ... release o; end