A Hardware Description Language, abbreviated as HDL, is a language used to describe a digital system. For example, a network switch, a microprocessor, or a memory, or a simple flip-flop. This means that, by using an HDL, one can describe any (digital) hardware at any level. VHDL is an HDL. And so is Verilog.
In this article, we will first discuss some basic constructs and syntax in Verilog, which provide the necessary framework for Verilog programming. Then we learn about the different data types available in Verilog. Then, we move on to learn about the module and port declarations from the Verilog point of view. These are some important topics to know about for Verilog coding purposes.
Have you noticed some words turn red in Vivado GUI? Those are reserved words in Verilog known as keywords. We can use them to define language constructs.
Verilog provides us with a set of keywords. These keywords have a predefined purpose that is understood by Verilog compilers across the board. For example, for defining a module, we use the keyword
module. Whenever you use that keyword, the compiler expects you to define a module.
Always write keywords using lowercase letters. Why? Unlike VHDL, Verilog is a case sensitive language. For example:
input wire // wire is a keyword input WIRE // WIRE is not a keyword
We can see some commonly used keywords in Verilog from the table below:
An identifier is a unique name, which identifies an object. They are case-sensitive made up of alphanumeric characters, underscore, or a dollar sign.
We can start identifiers using alphanumeric or underscore. It’s not possible to name identifiers beginning with a dollar sign since it is reserved for naming system tasks. Also starting with numbers is not advisable. Let’s see how we can use identifiers in Verilog:
reg value //value is an identifier input CLK // CLK is an identifier input $time //not an identifier but a system task output out1 //out1 is an identifier output 123abc // invalid identifier:-starting with numbers
Another type of identifier that exists is the escape identifier. Escaped identifiers start with a backslash and end with white space (i.e., space, tab, newline).
Escaped identifiers can contain any printable characters. The backslash and white space are not part of the identifier. For example:
We can use two types of number specification in Verilog:
The format for writing sized number is:
The size specifies the number of bits in the number. It is written in decimal only.
The base format is used for representing which base we use to represent our number. Legal base formats are binary(‘B or ‘b), decimal(‘d or ‘D), octal(‘O or ‘o) or hexadecimal(‘h or ‘H).
The number is specified as consecutive digits from 0,1,2,3,4,5,6,7,8,9,a,b,c,d,e,f. Only a subset of these digits is legal for each base. Let’s see the legal digits for each base.
Let’s see how we can specify sized numbers in Verilog:
4'b1111 // 4-digit binary number 12'habc // 12-bit hexadecimal number 16'd255 //16-bit decimal number 16'hDEF //16-bit haxadecimal number. Uppercase legal for number specification
Numbers without <size> have a default size of 32 bits. The default size differs depending on the machine and simulator. When no base is specified, it is decimal by default. For example:
23456 //32-bit decimal number by default 'h5c //32-bit hexadecimal number 'o21 //32-bit octal number
It is recommended to use sized numbers. As a designer, we always thrive to use our memory allocation efficiently to build efficient hardware.
Isn’t it better to write
The unsized format will take up 32-bit of memory, which is a total waste in this case. Hence, we prefer sized number specifications.
X or Z values
For representing ‘unknown’ and ‘high impedance,’ Verilog uses x and z. These are important for modeling real circuits. An x or z sets four bits in the hexadecimal base, three bits in the octal base, and one bit in the binary base. For example:
12'h13x //12-bit hex number; 4 least significant bits unknown 6'hx //6-bit hex number 32'bz// 32 bit high impedance number
For describing or documenting a model, we use comments. This part of the code will be skipped during execution. For example:
c = a+b // values of a and b are added and result is stored in c
We can write comments in two ways. When we want to skip one full line during execution, we specify the comment using //. Verilog jumps from that point to the end of the line.
To write multiline comments, we start with /* and end it with */.
We can see how comments are used in Verilog:
//This is a one-line comment /* this is a multi-line comment */ /*this is /*not a */ legal comment */ //Note:- Multi-line comments cannot be nested /*this is a // legal comment */ //one-line comments can be embedded in multi-line comments
That’s all about some basic conventions and elementary syntax in Verilog. Now, let’s see what the different data types available in Verilog are.
Data types in Verilog inform the compiler whether to act as a transmission line (like a wire) or store data (like a flip flop). This depends on what group of data type is declared. There are two groups: NET and REGISTER. Let’s discuss them.
Recall that the entire purpose of an HDL like Verilog is to describe circuits. So physical circuit elements that you see need to be describable by the HDL.
A Net represents connections between hardware elements. Nets don’t store values. They have the values of the drivers.
For instance, in the figure, the net out connected to the output is driven by the driving output value A&B.
A net data type must be used when a signal is:
- driven by the output of some devices.
- declared as an input or inout port.
- on the left-hand side of a continuous assignment statement.
Types of Nets
wire is the most commonly used net in modeling circuits. When used in the code, it exhibits the same property as an electrical wire used for making connections. Let’s see how we can declare
wire by describing a simple AND gate in Verilog:
module and_gate; input wire A,B; output wire C; assign C = A & B; endmodule
This is another type of net that has identical syntax and function as
Then, what’s the difference?
wire net can be used for nets that are driven by a single gate or continuous assignment. Whereas, the
tri net can be used where multiple drivers drive a net.
Let’s see an example where
tri is declared:
The above diagram shows a 2:1 mux constructed using tri-state buffers. Let’s see how we can write the Verilog code for the above circuit.
module 2:1_mux(out, a,b, control); output out; input a,b,control; tri out; wire a,b,control; bufif0 b1(out, a, control); //b1 drives a when control = 0;z otherwise bufif1 b2(out, b, control); //b2 drives b when control = 1;z otherwise endmodule
Have you noticed something?
We have declared out as
tri because it is driven by multiple drivers as per the logic circuit.
We have instantiated tri-state buffers
bufif0 to implement the functionality of the MUX.
The net driven by b1 and b2 works in a complementary manner. When b1 drives a, b2 is tristated; when b2 drives a, b1 is tristated. Interesting, isn’t it?
Are you hearing about the tri-state buffer for the first time? Don’t worry. Here we go.
supply0 and supply1
In every circuit we build, there are two crucial elements – power supply and ground. Hence, we use supply0 and supply1 nets to represent these two. The supply1 is used to model power supply. Whereas, the supply0 is for modeling ground.
These nets have constant logic values and strength level supply, which is the strongest of all strength levels.
Let’s see how we can use them:
supply1 Vcc; // all nets connected to Vcc are connected to power supply supply0 GND; // all nets connected to GND are connected to ground
The variable datatypes are responsible for the storage of values in the design. It is used for the variables, whose values are assigned inside the
always block. Also, the input port can not be defined as a variable group.
integer are examples of the variable datatypes. For designing purposes, we commonly use
Isn’t that hardware registers made from flip flops? No.
In Verilog, the term register means a variable that can hold value. It can retain value until another value is placed. Unlike a net, a register does not need a driver. It doesn’t need a clock as hardware registers do.
Register datatype is commonly declared using the keyword
reg. Let’s see how we can declare
reg in Verilog:
reg reset // declare a variable reset that can hold it's value initial begin reset = 1'b1; //initialize the reset variable to 1 to reset the digital circuit #10 reset = 1'b0; //After 10 time units, reset is deasserted. end
Integer, Real and Time Register Data Types
An integer is a general-purpose register data type used for manipulating quantities. The integer register is a 32-bit wide data type. So what makes it different from reg? The reg store values as unsigned quantities, whereas integer stores signed values.
To declare an integer in Verilog, we use the keyword
integer. For example:
integer counter; //general purpose variable used as a counter; initial counter = -1 // A negative one is stored in the counter.
The real register is a 64-bit wide data type. In order to store decimal notation(eg 3.14) or scientific notation(eg: 3e6 which is 3 X 106 ), we use the keyword
real. Real numbers cannot have a range declaration, and their default value is 0.
Let’s see how we can declare them Verilog:
real delta; // define a real variable called delta initial begin delta = 4e10; //assigned scientific notation delta = 2.13 //assigned a decimal value of 2.13 end
If a real value is assigned to an integer, it gets rounded off to the nearest integer. For example:
integer i; initial begin i = delta // i gets the value 2 (rounded value of 2.13); end
Since timing is an essential factor in designing digital circuits, we need something to keep track of it. Hence, the keyword
time helps us to record the simulation time. The default size of time-register is implementation-specific but should be at least 64 bit.
To get the current simulation time, the
$time system function is invoked.
Let’s see how:
time save_sim_time; //Define a time variable save_sim_time initial begin save_sim_time = $time; // Save the current simulation time. end
realtime register is also a time register which will record current simulation time. For example
realtime save_sim_time; //Defining the variable initial begin save_sim_time = $realtime // The system task $realtime will invode real value of the current simulation time end
Scalar and Vector datatypes
1-bit is declared as a scalar datatype. It has only one bit. When we declare a wire or reg as a scalar, we write them as:
wire n; reg d1;
Vector data types are multi-bit. We either use [<MSB bit number> : <LSB bit number>] or [<LSB bit number> : <MSB bit position>] to represent them. For example, we can declare a 4-bit wire or reg as:
wire [0:3] a; reg [3:0] d0;
Want to see how wire and reg in scalar data type look like?
We have now learned about data types in Verilog. Now we move on to learn about the most important topics in Verilog; the module. We’ll learn how to declare it and what are the essential components associated with it.
The module forms the building block of a Verilog design. A module definition always starts with the keyword
module and ends with
endmodule. There are five components within a module. They are:
All these components except the
module, module_name, and
endmodule can be mixed and matched as per design needs.
To get a better understanding, we can see the Verilog code of 2:1 Multiplexer:
//This code is used to illustrate different components in Verilog //module and port list //mux_21 module module mux_21(s,a,b,out) //port declarations input s,a,b; output out; //using assign statement and conditional operator in dataflow modeling assign s? a:b; //endmodule statement endmodule
Have you noticed something? There are no variable declarations, instantiation of lower gates, or behavioral blocks mentioned in the above module.
Let’s see the test bench treating the above DUT.
//module name //mux_21_tb module module mux_21_tb; //declaration of reg,wire and other variables reg S, A, B; wire OUT; //Instantiate lower level modules //In this case, instantiate the mux_21 module mux_21 dut(.s(S), .a(A), .b(B), .out(OUT)); //behavioral block initial initial begin $monitor("simtime = %g, S = %b, A = %b, B = %b, OUT = %b", $time, s, a, b, out); #5 s = 0; #5 a = 0; b = 0; #5 s = 1; #5 a = 0; b = 1; #5 a = 1; b = 0; #5 s = 0; end //endmodule statement endmodule
The test bench module above contains the variable declaration, behavioral block, and instantiation of lower blocks. But there is no port declaration and dataflow statements in this module.
What can we understand from that?
It’s not compulsory that all the five components should be incorporated in the module. The components except for the
module, module_name, and
endmodule are mixed and matched according to what our design demands.
IO Port Declaration
Let’s take a small piece of code from the mux_21 module
module mux_21(s,a,b,out); input s,a,b; output out;
The above code has specified that there are ports in our
Wait? What are the ports?
Ports are what declares the interface of the module. The ports model the pins of the hardware components. For example, our
mux_21 has four ports communicating with its internal circuitry.
Depending on their direction, ports can be input, output, or inout. As per our
mux_21 module, s, a, and b are input ports, while out is the output port.
What about the ports in the
mux_21_tb (testbench) module? It is a stimulus module. Hence we use reg and wire declarations. Declaring ports is not applicable in this case.
We follow the below format for declaring I/O ports:
<port_direction> <port datatype> <port size> <port name>
Let’s see some examples of how I/O ports are declared:
D flip flop
input clk,d,rest; output reg q, qbar;
If the data type is not specified, it is declared as
4-bit Full Adder
input [3:0] A,B; input Cin; output [3:0] SUM; output Cout;
We can also declare ports using ANSI C style. In this style, the complete information of the port is specified along with the module and port declarations. Such an example is given below:
module full_adder_4(input [3:0] A, B, input Cin, output [3:0] SUM, output Cout); //module internals ......... endmodule
There are two approaches in design: Top-down and Bottom-up approach.
In the top-down approach, we build the top-level block and identify the sub-blocks until we come to leaf-cells, which cannot be divided further. In the bottom-up approach, the leaf cells are first designed, then the lower blocks, which is then combined to form the top-level blocks.
Let’s say we need to build a full adder. When we use the top-down approach, we first design the full adder using two half adders, and then we design the half adders using logic gates. The bottom-up approach suggests that we design the half adder from the logic gates and then build the full adder using these half adder blocks.
In short, we have understood we can build a top-level module from lower-level modules and vice-versa. To do that, Verilog has provided us with a feature-Module Instantiation. This enables us to nest lower modules to form a top-level module.
The syntax for module instantiation.
To nest one module to another, we follow the below syntax.
<lower module name> <instance name> (<ports from top-level module>)
You will get a better idea with the example of describing a full adder:
To build a full adder, we need two half adders.
Hence, let’s build a half adder using gate primitives.
First, as for any Verilog code, we declare the module and ports:
module ha(sum,carry,a,b); input a,b; output sum,carry; ...... endmodule
A half adder requires an XOR gate for summing two inputs and an AND gate to generate carry. Hence using gate primitives, we write as
So, our Half adder module will be like this:
module ha(sum,carry,a,b); input a,b; output sum,carry xor(sum,a,b); and(carry,a,b); endmodule
Now, we can use this to build a full adder from the ha module referring the logic diagram:
As usual, carry out the module and port declaration:
module fa(SUM,Cout,A,B,Cin); input A,B,Cin; output SUM, Cout; .... endmodule
Now let’s instantiate the
ha module to the full adder module:
ha ha1(sum1,c1,A,B); ha ha2(SUM,c2,sum1,Cin); or(Cout,c1,c2);
Hence, our code is:
module fa(SUM,Cout,A,B,Cin); input A,B,Cin; output SUM, Cout; ha ha1(sum1,c1,A,B); ha ha2(SUM,c2,sum1,Cin); or(Cout,c1,c2);
Have you noticed something?
When we instantiated, we connected the ports in the
fa module the same order as the ports declared in the
ha module. This is what we call instantiation by port order.
There’s another way of nesting to modules. Let’s see how we can connect the same
ha modules to build
fa module using this style
ha ha1(.sum(sum1), .carry(c1), .a(A), .b(B)); ha ha2(.sum(SUM), .carry(c2), .a(sum1), .b(Cin)); or(Cout, c1, c2);
Unlike the former, this style doesn’t mind if the order is messed up. But, we have mentioned the names of all the physical connections related to both modules. Hence, this style is called instantiation by port name.
I hope you had fun learning about the syntax used in Verilog and the various data types in the HDL. If you have any doubts, feel free to drop comments.