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.

Pages
Contributors: Toni_K
Favorited Favorite 4

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:

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.

  1. 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.
  2. 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.
  3. 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:

  1. The always is a term that means, "Always do this block of code every time the clk input changes on a positive edge."
  2. To make sure we are synchronous, we need to handle a reset signal, and that is where the if(rst) comes in.
  3. Blocks of code don't use { or } for denoting the end of the function. Rather, in Verilog, it's the begin and end. Verilog is similar to C/C++ in that assumes the first line after a command is nested inside that command.
  4. 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.

  1. 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.
  2. 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] and count_up[1]).
  3. Now, this code implements 2 independent counters, with counter1 and counter2. 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:

  1. Safe states, so if a glitch occurs, it goes to a state in which the system can recover easily.
  2. Allows for better optimization on designs.
  3. Resets are forced to follow clock cycles.

Disadvantages:

  1. Multi-clock designs will have multiple resets, which might not be tied together
  2. Requires a clock at all times.
  3. Potential higher use of tri-state buffering

Asynchronous

Advantages:

  1. Faster capabilities with data (data will generally be more correct)
  2. Removal of reset from the data path (path in which data follows the clock)
  3. Multi-clock designs only need 1 system reset

Disadvantages:

  1. Simulations might not work appropriately
  2. Has to be generated from I/O, never internally.
  3. 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.