Applications 2
Exercise 1 : 1 bit adder
Module declaration
Any Verilog circuit or block is enclosed within the keywords module and endmodule. The module keyword is followed by the name of the module. This name begins with a letter (Verilog is case-sensitive) or underscore (_), uses alphanumeric characters and underscores and should be a descriptive one (adder, adder_32, counter, lshift_register, Serial_to_Parallel_converter).
Right after the module name comes the interface. The interface is the list of port declarations, separated by commas and enclosed within parentheses. It is strongly recommended to declare each port on a separate line, even if some ports have the same direction and size. Also it is recommended to group the declarations by their direction. A port declaration starts with the keyword that defines the port's direction: input or output. For one-bit ports the declaration comprises only the direction and the name. Port names follow the same rules as module names. A port name should be a unique identifier inside the module's code - its scope is limited to the module.
A multi-bit port declaration defines the width of the port through two indices within brackets and separated by a colon: [msb:lsb]. The first index denotes the most significant bit (MSB), the second one is the least significant bit. MSB should always be greater than MSB. Usually the LSB index is 0, therefore if the width of the port is N, the first index would be N-1.
The module declaration comprises the keyword module, the module name and its interface, and ends with a semicolon:
module adder ( // keyword '''module''' followed by the module's name and the opening parenthesis of the interface
input a, // one-bit input port named 'a'
input b, // one-bit input port named 'b'
output [1:0] c // two-bit output port. Bit 1 is MSB, bit 0 is LSB. There is no comma after the last port declaration
); // the closing parenthesis of the interface and the semicolon that ends the module's declaration
Module description
Anything written between the module's declaration and the keyword endmodule is part of the module's description.
A module may be described by what it does. This is a behavioral description. Very simple modules could be described using a single instruction, that assigns to the outputs the result value of an expression over the inputs. Because Verilog is a description language, not a programming language, the assignments that describe how logic values are generated from other logic values are attached to special Verilog keywords. One such keyword is assign. It is followed by a single assignment: assign y = E(x1, x2, ... xN) It is forbidden to use the left-hand variable inside the right-hand expression.
The one-bit adder may be described by a simple expression that computes the output value as the addition of its inputs:
assign c = a + b; // variable c is always equal to the sum of a and b
The assign statement describes something that is continuously evaluated. It may be viewed as a small black box with an output and some inputs. The left-hand variable is updated immediately whenever any of the variables in the right-hand expression changes its value. The physical circuit that implements the assign statement is a combinational logic circuit that reacts immediately to any change of its inputs.
The whole code of the adder module:
module adder (
input a,
input b,
output [1:0] c
);
assign c = a + b;
endmodule
Testbench module
The testbench is another module, therefore it is defined in a separate file. Always follow these rules:
- each module is defined in a separate file
- the name of the file is the name of the module
The testbench has no interface. All signals are generated internally. Therefore the testbench declaration has an empty interface list, module testbenchName();, or the parentheses may be missing at all, module testbenchName;
It is recommended to include in the name of the testbench the name of the module to be tested. The testbench for the adder module may be named adder_testbench, test_adder, adder_tb, a.s.o.
As for any module, the description follows the module declaration. The testbench instantiates the module to be tested and generates values for the signals connected to the inputs of the tested module. The testbench is used only in simulation, it cannot be synthesized. However, this restriction is turned to an advantage because the testbench may use procedural statements freely to generate whatever stimuli are desired.
The DUT (device under test) is a block inside the testbench module. It is an instance of a module. The module is like a blueprint, while the instance is a materialization of that blueprint. For those familiar with object-oriented programming this may sound familiar. The module is like a class, and the instance is an object of that type.
A module instantiation starts with the moduleName followed by the instanceName and the list of its external connections enclosed within parentheses. Usually, the module instantiated in the testbench is named dut (device under test).
The list of connections connects each port of the instance to a signal of the testbench. It is strongly recommended to write each connection on a separate line. You may write the connections in the same order as in the interface declaration for that module. You should have at most ONE connection for each port, but some ports may be leaved unconnected if they are not used. A connection is declared with a dot (.) immediately followed by the name of the port and finally the name of the signal connected to that port, enclosed in parentheses: .modulePin(varName) . The connections in the list are separated by commas.
The adder module is instantiated with the name dut. Its inputs receive signals num1 and num2, and its output is connected to a wire named result:
adder dut ( // a module of type 'adder' is instantiated. The name of this instance is 'dut'
.a (num1 ), // port 'a' is connected to signal 'num1'
.b (num2 ), // port 'b' is connected to signal 'num2'
.c (result) // port 'c' is connected to 'result'. There is no comma after the last connection
); // the closing parenthesis of the interface and the semicolon that ends the module's instantiation
All signals and wires connected to an instance should be declared, and the declarations should precede the instantiation.
The signals connected to the instance inputs are stimuli generated by the testbench. They should be declared as variables of type reg. The instance outputs should be declared as variables of type wire. The variable declaration begins with the type (reg or wire) and ends with the variable's name. We strongly recommend to declare each variable in a separate line. Also, you should NOT initialize a variable in the declaration statement (keep in mind that Verilog is a description language, not a programming language). For the multi-bit signals the declaration must specify the width, in the same way you declare the width of a module multi-bit port.
reg num1; // one-bit variable of type reg and named 'num1'
reg num2; // one-bit variable of type reg and named 'num2'
wire [1:0] result; // two-bit variable of type wire and named 'result'
As a rule of thumb, keep in mind that any output of an instance could be declared only as a wire, whereas the inputs to an instance are declared of type reg if you give them values using explicit assignment statements.
The stimuli for the adder block are generated as in Laboratory 1, using a Verilog initial block, inside which you assign values to signals num1 and num2. To thoroughly test the adder block it is useful to generate all possible combinations of values for those two signals. One such sequence may be 00, 01, 10, 11:
initial begin
num1 = 1'b0; num2 = 1'b0;
#5 num1 = 1'b0; num2 = 1'b1;
#5 num1 = 1'b1; num2 = 1'b0;
#5 num1 = 1'b1; num2 = 1'b1;
#5 $stop;
end
As in Laboratory 1 you may generate the values for num1 and num2 in a single statement, combining them into one two-bit bundle.
The whole code of the adder testbench module:
module adder_tb;
// declarations
reg num1;
reg num2;
wire [1:0] result;
// instantion
adder dut (
.a (num1 ),
.b (num2 ),
.c (result)
);
// stimuli generation
initial begin
{num1, num2} = 2'b00;
#5 {num1, num2} = 2'b01;
#5 {num1, num2} = 2'b10;
#5 {num1, num2} = 2'b11;
#5 $stop;
end
endmodule
Module simulation
Simulate the adder_tb testbench and check on the waveforms that the output of the adder module is indeed the sum of its input values.
Implementation
To implement the adder on the FPGA and to interact with it we should imagine some means to set and to change the adder input values and to see the value of its output. The FPGA board allows us to interact with the circuit implemented inside the FPGA through a couple of switches, some push buttons and various LEDs that are connected to FPGA pins, which may be configured to be wired inside the FPGA to the ports of the implemented module. The FPGA pins assignment is done through the constraints file. For each pin assignment there is a line that instructs the implementation tool to connect the port with a given name to a particular FPGA pin, and also sets the parameters of the I/O pad of that pin. For example
set_property -dict { PACKAGE_PIN M20 IOSTANDARD LVCMOS33 } [get_ports { num1 }];
is a constraint that connects the port num1 to the FPGA pin labelled M20.
Hopefully, for each FPGA board you may find constraints files with constraints for all FPGA pins, with indications as to which switch, button, LED, a.s.o. is connected towhich FPGA pin. All constraints are commented. You should uncomment only the lines for the switches, buttons, LEDs a.s.o. that you want to use, and replace the port names with the names of ports from your module. For multi-bit ports there must be one separate connection constraint for each port bit, since each port bit needs a separate FPGA pin.
The adder will be tested using two switches to set the input values, and two LEDs to show the output two-bit value:
##Switches
set_property -dict { PACKAGE_PIN M20 IOSTANDARD LVCMOS33 } [get_ports { a }]; #SW 0
set_property -dict { PACKAGE_PIN M19 IOSTANDARD LVCMOS33 } [get_ports { b }]; #SW 1
##LEDs
set_property -dict { PACKAGE_PIN R14 IOSTANDARD LVCMOS33 } [get_ports { c[0] }]; #LED 0
set_property -dict { PACKAGE_PIN P14 IOSTANDARD LVCMOS33 } [get_ports { c[1] }]; #LED 1
Exercise 2
design source file for the 2 by 2 bit multiplier
- operators in expressions
testbench source file for the 2 by 2 bit multiplier
- control instructions: repeat (nrOfIterations)
- parallel initial blocks
Exercise 3
design source file for top-level entity Multilevel hierarchy. Mixed description: top-level - structural description, low-level - behavioral description.
- same module type, different instance names
- internal wires for interinstance connections
testbench source file for the top-level entity
- concatenation operator {varName1, varName2, ...}
- $monitor(%b...,varName1,...)
- format specifiers %b for binary (logic) values, %d for decimal values