Programming FPGAs: Papilio Pro
This Tutorial is Retired!
This tutorial covers concepts or technologies that are no longer current. It's still here for you to read and enjoy, but may not be as useful as our newest tutorials.
Basic Verilog
This section will go over the basic syntax for Verilog and the basic elements. This is a subset of all the capabilities of Verilog, and I highly recommend reading more into it to understand the power it has. Comparisons will be made between the C/C++ syntax and Verilog.
Note: the code provided in this section is not in the example project. This code is to just show the basics.
Module Definition
In the C++ world, a function is defined as follows:
void function_name(data_type param1, data_type param2) {
// Code Goes Here
}
In Verilog, this is how a module is defined:
module module_name(
input clk,
input rst,
input [7:0] data_in,
output [7:0] data_out
);
// Module code goes here
endmodule
As you can see, there are clear differences and similarities. For starters, the input
and output
are the I/O ports of this module. You can define as many as you want. For buses, you use the [#:#]
, the [7:0]
in this case, and adjust how big it is.
Like C++ functions, modules can be called within other modules. The main
function of a C/C++ program is a function, so a Verilog module can be the "main" function of the design. This "main" function is referred to as the "top level" of the design. Top levels follow the behavior of top-down design, bottom-up implementation.
Top-down design is the process of taking your design and breaking it up into smaller components, allowing you to then reuse them. It's the same as writing C/C++ functions to do specific tasks.
I/O Ports
As shown in the previous segment, this list contains the types of I/O ports: input
, output
, inout
. These are like data types in C/C++ and help define the flow of data in the module.
Each is unique, especially inout
, but generally, you will only use input
and output
.
In the module defined above, we have 3 input
ports and 1 output
port. The input
has a bus size of 8 (remember, in electronics and programming, we count from 0). The output
has the same size bus.
Now that we have a good framework to begin writing a module, let's build a simple synchronous counter.
Counters
A counter, as you can guess, is something that counts. There are different ways to count, each having their own benefits. The different types of counters are:
- Binary-coded Decimal (BCD) Counter
- Binary Up/Down Counter
- Hex/Oct Counter
This list is not complete, because people can write their own counters to do what they want. For us, we'll do a typical binary up/down counter.
To understand counters, here's a typical C++ version we'll convert to Verilog.
for(int i = 0; i < MAX; i++) {
if( count_up == 1)
counter += 1;
else if ( count_up == 0 && counter != 0)
counter -= 1;
}
First and foremost, let's break down what it is doing so we can apply this knowledge later.
- We are initializing the for-loop's parameters to start at 0 and go to MAX. We don't know what MAX is, but we will not count higher than that. And, we'll only increment by 1.
- We are checking to see if the variable
count_up
is set to 1. If so, we increment the count up, otherwise, we will count down. - If we count down, we need to make sure
counter
is not 0. The reason is because 0 is the lowest we can count.
Now, let's look at the equivalent code for Verilog.
module counter_mod(
input clk,
input rst,
input count_up,
output reg [MAX-1:0] counter
);
parameter MAX = 8;
always @(posedge clk) begin
if(rst) counter <= 0;
else begin
if(count_up == 1)
counter <= counter + 1;
else if(count_up == 0 && counter != 0)
counter <= counter - 1;
else
counter <= 0;
end
end
endmodule
The module name is counter_mod,
and it has 3 input
and 1 output
ports. Don't worry about some of the uniqueness of the I/O Ports (they can get complicated, I recommend Googling if you have questions).
We also have a parameter
, which will be discussed later.
Finally, we have the meat of the module, the actual counter. One key concept a lot of people fail to understand about HDL is that the code executes in parallel, but is written sequentially.
Let's break down the code:
- The
always
is a term that means, "Always do this block of code every time the clk input changes on a positive edge." - To make sure we are synchronous, we need to handle a reset signal, and that is where the
if(rst)
comes in. - Blocks of code don't use
{
or}
for denoting the end of the function. Rather, in Verilog, it's thebegin
andend
. Verilog is similar to C/C++ in that assumes the first line after a command is nested inside that command. - Now the actual code of counting. Notice how it's extremely similar to the C++ version?
You might be asking yourself, "What is with that extra else
statement?" The answer is latches. In synchronous designs, we can sometimes write code that might accidentally create memory elements called latches. These latches aren't bad, but they're not expected (they are really glitches in the system). In order to not have a glitch, we need to put the counter at a reset state.
Submodule Instantiation
As stated earlier, modules can be called, or instantiated, in other modules like functions within functions in C++, and it follows the same basic idea.
In C++:
void function_1( data_type param1, data_type param2) {
// Code Goes Here
}
void function_2( data_type param1, data_type param2) {
// Code Goes Here
}
void function_3( data_type param1, data_type param2) {
function_1(param1, param2);
function_2(param1, param2);
}
Since the counter we wrote could be considered a top-level module, I want to make a couple instantiations. To do this:
module top (
input clk,
input rst,
input [1:0] count_up,
output [MAX-1:0] counter1,
output [MAX-1:0] counter2
);
parameter MAX = 8;
// First counter
counter_mod #(
.MAX(MAX)
) counter1 (
.clk(clk),
.rst(rst),
.count_up(count_up[0]),
.counter(counter1)
);
// Second counter
counter_mod #(
.MAX(MAX)
) counter2 (
.clk(clk),
.rst(rst),
.count_up(count_up[1]),
.counter(counter2)
);
endmodule
Let's break down the code we just added.
- Like in C++, we use the module name to start off, but the first parenthesis defines module parameters (again explained later). Then comes the I/O. In C/C++, the position of the parameter defines what variable it's tied to, and Verilog can do it this way, but it's not the proper way to do so. For Verilog, we use the name of the instantiation model I/O port (eg.
clk
with a signal tied to it). This allows us to build complex hierarchy. - Remember the buses? Well, instead of writing out individual ports for
count_up
, I created a bus, which allows me to reuse the name but select the individual signal (count_up[0]
andcount_up[1]
). - Now, this code implements 2 independent counters, with
counter1
andcounter2
. We can do a tri-state, and have it alternate between the two, but for this tutorial, this is enough.
Parameters of Modules
The concept of a parameter is the equivalent to a const
variable in C/C++. A const
variable is a variable in C++ that takes up memory but can never be altered by the program.
C++ syntax:
const int MAX = 8;
Verilog:
parameter MAX = 8;
As with the const
, parameter
cannot have the value adjusted on the fly during compilation. Unlike a computer program, FPGAs cannot handle dynamic memory allocation, so everything must be defined.
Note: Verilog has macro capability which is sometimes used in place of parameters. As an example:
`define MAX 8
module (
...
input [`MAX-1:0] data_in,
...
);
...
endmodule
However, unlike the macros in C/C++, a ` mark has to be used to denote if it's a macro or not.
Generally, people use macros to simplify segments of Verilog to be more readable.
Synchronous/Asynchronous Logic
This section will go into detail on the synchronous/asynchronous logic with respect to FPGAs.
Generally, digital logic designers follow this advice: K.I.S.S. = Keep It Synchronous, Stupid.
What are the advantages and disadvantages for the two?
Synchronous
Advantages:
- Safe states, so if a glitch occurs, it goes to a state in which the system can recover easily.
- Allows for better optimization on designs.
- Resets are forced to follow clock cycles.
Disadvantages:
- Multi-clock designs will have multiple resets, which might not be tied together
- Requires a clock at all times.
- Potential higher use of tri-state buffering
Asynchronous
Advantages:
- Faster capabilities with data (data will generally be more correct)
- Removal of reset from the data path (path in which data follows the clock)
- Multi-clock designs only need 1 system reset
Disadvantages:
- Simulations might not work appropriately
- Has to be generated from I/O, never internally.
- De-asserting a reset signal can cause problems.
Which option is best for you? The best answer if you are just getting started with FPGAs is to run synchronously. Remember, K.I.S.S.
The reason for keeping things synchronous is because of the inherit inability to know what the system does if there is no "safe state." Let's look at the counter code with an asynchronous reset.
always @(posedge clk or negedge rst) begin
if(!rst) counter <= 0;
else begin
if(count_up == 1)
counter <= counter + 1;
else if(count_up == 0 && counter != 0)
counter <= counter - 1;
else
counter <= 0;
end
end
If this system were to hit the reset signal, once the de-assertion of it occurs (goes from positive to negative; hence the negedge
), the system stops what it is doing and immediately sets counter
to all zeroes. Now the propagation delay (time it takes for data to travel from the input to the output), will be different for every element on the FPGA. On a big design, it could take nanoseconds to finish de-asserting the reset, and, by then, data could potentially get corrupted or glitches may occur.
Timing plays a very large role in FPGA design. Timing is the idea that your design will make sure data gets to where it needs to go in the appropriate time.