Introduction: Digilent Pmod R2R Sine Wave Generator

We will create a Verilog project for the Digilent Zybo to create a 1 Kilohertz sine wave on the output pins of a Digilent Pmod R2R. This project is intended as a introduction to production of sine waves using an FPGA, a critical step in starting a number of more complex projects, such as driving audio hardware and other analog circuits. We will also use a new feature of Vivado 2016.2, Add Module in the IP Integrator, which makes prototyping of complex projects relatively easy, as we will be able to skip the step of writing an HDL wrapper file and easily see how our modules are connected together and interact.

The project is provided, build-able through a TCL script. If you only want the source code, the HDL (hardware descriptive language) files can be found under src/hdl. In order to use the IP Integrator and generate a bitstream file, the project will need to be built through Vivado's TCL console. A good tutorial for this process can be found in this Instructable. Downloading and building this project is not required though, as we will walk through what is required for you to create the project yourself.

Step 1: Project Overview

Materials Requirements List

  • Installed version of Vivado 2016.2 --Earlier versions will not work, as a new feature in the IPI, "Add Module", will be used, though you may still be able to follow along and write a custom HDL wrapper.
  • A Digilent Zybo (or other compatible Digilent FPGA).
  • Your Digilent PmodR2R.
  • A master XDC file for your Digilent board -- available through the board's page on the Digilent Wiki..
  • A Python environment.

Functional Description

The PmodR2R allows us to output voltages in a range between its ground and VDD levels. We pass the R2R 8 bits of input, ranging from 0 to 0xFF (255 in hexadecimal). It then outputs a voltage corresponding to our input, passing a zero means it will output ground, passing 0xFF means it will output 3.3V, the typical supply voltage for the Pmod headers on Digilent boards.

In order to produce a 3.3V zero-to-peak sine wave, we will need to repeatedly count through a single sin wave scaled to the range 0 to 0xFF. The easiest way to do this in Verilog will be to create a lookup table, basically just an array, filled with the correct values in order. We would then count through the array, outputting each value in order. Since we also want to control the frequency of our sine wave, we will need a way to control the speed at which we count through our sine wave. The easiest way to do this is to only increment our counter every so often, which we can achieve by enabling it only when another counter hits a maximum value. We will talk about how to implement this in Verilog later.

This design will give us essentially two variables we can use to control the speed and resolution of our sine wave in our lookup table size and maximum count of our "clock divider" counter. Depending on the selected values, we are placing a cap on the frequencies available at the R2R output, and the higher we decide to place this cap the worse the resolution of our signal will be. With the selected target frequency of 1KHz, we will be able to ignore this tradeoff and produce a reasonable result. We will discuss the selection of these parameters later.

Step 2: Creating the Project

After loading up Vivado, in order to get the project set up, follow these steps:

  1. Select "Create New Project".
  2. Select "Next", add a name for your project, without spaces, and select "Next".
  3. Make Sure "RTL Project" and "Do not specify sources at this time" are selected. Select "Next".
  4. Select Boards, and find and select your board's part file. Select "Next".
  5. Select "Finish".

With this Vivado will have created a new empty project for you.

Step 3: HDL Counter and Lookup Table

Before we can start writing our HDL code, we need to create the files. In order to do this, follow these steps:

  1. In the Project Manager "Sources" subwindow, right click on "Design Sources" and select "Add Sources".
  2. Select "Add or create design sources" and click Next.
  3. Select "Create File".
  4. Set file type as Verilog, file name as counter, and file location as .
  5. Repeat step 4 for another file named lut.
  6. The Define Module window will pop up, we can immediately click OK, then Yes at the prompt that will pop up.

You have just created two new files with empty module templates, so next we will add HDL code to turn them into functional blocks for later use.

We need three components to be able to produce a digital sine wave. A lookup table will allow us to convert a counter output to a digital sin wave -- just like indexing into an array. We will need one counter to generate the lookup table address, and a second counter to control the speed of the first counter.

Our counter needs several features. First, it needs to be able to increment an output bus, so we will add a clock source and an output register. We need to be able to control how often this happens, so we will add an enable pin. We also want to be able to configure the size and maximum value of the count register differently for different counters, so we will add width and max value parameters. We also want an output representing that the counter is at it's maximum value, this will allow us to only increment our second counter when the first counter completes a rollover loop -- counting all of the way through it's range and resetting itself -- I will refer to this register as "tc", for "terminal count". The following code defines the counter module and it's ports:

`timescale 1ns / 1ps

module counter (
	input clk,
	input rst,
	input en,
	output reg [DATA_WIDTH-1:0] data,
	output reg tc
);
parameter DATA_WIDTH=8;
parameter DATA_MAX=255;

//functional code goes here

endmodule

While we are required to give the two parameters default values, we will be able to change them for each counter later.

The functional description described can be achieved with two "always blocks". The first always block will set the tc register whenever the data bus changes. The tc block should not be clocked, so that we can use it in our counter logic, without worrying about accidental "off by one" errors. The second always block will be clocked, whenever a clock edge occurs, it will increment or reset the counter from maximum, as long as enable is high, otherwise, it will hold it's current value. It should also trigger on resets, so that it will be set low as soon as the reset button is pushed.

always@(data)
	if (data == DATA_MAX)
		tc = 1'b1;
	else
		tc = 1'b0;

always@(posedge clk, posedge rst)
	if (rst == 1'b1)
		data <= 'b0;
	else if (en == 1'b0)
		data <= data;
	else if (tc == 1'b1)
		data <= 'b0;
	else
		data <= data + 1'b1;	

The approach toward building a lookup table is similar to that of the counter. It will need clock and address inputs, and an output data register. We also add a 256 byte block RAM by declaring a two dimensional register array.

`timescale 1ns / 1ps
module lut(
	input clk,
	input [ADDRESS_WIDTH-1:0] addr,
	output reg [DATA_WIDTH-1:0] data
);
parameter ADDRESS_WIDTH=8;
parameter DATA_WIDTH=8;
parameter FILENAME="sin.hex";

reg [DATA_WIDTH-1:0] mem [2**ADDRESS_WIDTH-1:0];

//functional code goes here

endmodule

The functional code for our lookup table is fairly simple:

initial $readmemh(FILENAME, mem);
always@(posedge clk)
	data <= mem[addr];

The initial read memory statement will load our file into our block ram as the FPGA is programmed. The always block will just set the data register to the value stored in blockram at our address.

Step 4: Look-Up Table Initialization File

For us to initialize the memory in our lookup table, we used readmemh, however we are still missing the file to be read using that function. Since we want to initialize every memory address, not skipping any cells, this file will follow a simple format, the data for each memory address will be stored on its own line, in ascending order by address. The data will be in hexadecimal format, so a line representing "for this address, output = 3.3V" will read "FF", since the PmodR2R will output the supply voltage when all of the lines on its data bus are high.

In order to automatically generate the file we will load into our lookup table, we can use Python.and it's built in math library. Since Python's math library's sin function takes radians as input and outputs a floating point number between -1 and 1, some scaling and conversion will be in order to write a hexadecimal integer string between 0 and FF to our file. We will count through our memory space, for each index, we will get a radian value between 0 and 2 pi. That radian value will be used with the sin function to get a float between 0 and 1, which will then be converted to an integer between 0 and 255. That integer will be converted to a hexadecimal string. In Python, these strings are generated with a leading "0x", so we will strip off the first two characters

import math
filename = "sin.hex"
f = open(filename, "w")
mem_len = 256
for i in range(mem_len):
	radians = math.pi * 2.0 * i / mem_len # get how far through the file we are, convert to radians
	fvalue = (math.sin(radians) + 1.0) / 2.0 # get sin value, in range 0.0-1.0
	ivalue = int(255 * fvalue) # convert from float to int, range from 0-0xFF
	s = hex(ivalue)[2:] # convert int calue to hexadecimal string and strip the leading "0x"
	f.write(s + "\n") # write to file
f.close()	

In order to run this code, follow these steps:

  1. Create a new file called singen.py somewhere on your computer.
  2. Copy the source into said file and save.
  3. Open a console window, navigate to the directory that contains the .py file.
  4. Call "python singen.py", or "py singen.py", depending on your python installation.

The script will have now created a new file named sin.hex in the directory containing singen.py. You can now add this .hex file to your Vivado project.

Step 5: The Block Design

Now for the fancy part. Create a new block design using the Flow Navigator under IP Integrator. Ensure that Design Name contains no spaces, that Directory is and that the source set is Design Sources.

With this block design created we can now draw our project design. Right click within the Diagram, and select "Add Module". This is a new feature to Vivado 2016.2, and lets us convert any sources we may have written into IP Cores automatically. We can add a module by just selecting it from the list of sources that pops up, and clicking OK. Use this method to add two counters and a lookup table. We should now connect these blocks together, and to input and output ports. Select one of the clk pins on one of the three blocks, and select Make External. You can now draw connections from each of the other clock pins to the new clock port. Do the same for the rst pins, so that each are tied to the same input. Tie the enable pin of counter_1 to the terminal count pin of the counter_0, and counter_1's data bus to the address bus of the lookup table. Make the lookup table's data bus external. Lastly, add a constant block, using "Add IP" and searching for "constant". Tie this constant block's output to the enable input of the counter_0. This constant block will ensure that counter_0 will always be running

Now that the design is drawn out, we need to go in and edit each block's parameters. This can be done by double clicking on the block in question, or right clicking the block, and selecting "Customize Block". We will be using the default values for the counter_1 data width and maximum count, and both the lookup table data width and address width. The maximum count for counter_0 depends on which board you are using. Since we are targeting 1KHz for the full sequence of counter_1, which only counts when counter_0 finishes its own sequence, we should use the following formula to determine our maximum count:

T_c0 = f_base_clk / (f_target * T_c1)

For the Digilent Zybo, our base clock frequency is 125MHz - a value which can be found in the board's reference manual on the Digilent Wiki, and which will likely be different for other FPGA boards - so with a target frequency of 1KHz and a lookup table size of 256 addresses, we get a counter_0 period of 488.28 clock cycles per address increment. Since this needs to be an integer, and our counter resets to zero, we will round down to get a maximum count of 488. To get counter_0's data width, we can to take the log base 2 of the maximum and round it up, telling us we need at least 9 bits to properly represent every value in the range 0 to 488. Customize counter_0 to fill in these values. Lastly, make sure that the constant block controlling counter_0's enable pin has width 1 and value 1.

Step 6: Adding an XDC

In order for Vivado to understand how our designs input and output ports are connected to the input and output pins of the boards FPGA, we need to add an XDC (Xilinx Design Constraint) file to our project. The easiest way to do this is to find and download the Master XDC for your board. The wiki page for your board on the Digilent Wiki should contain a link to this file on the sidebar under Design Resources. This file can be included in your project in the same way as adding a design source. Follow these steps:

  1. Right click on Constraints in the Sources subwindow. Select "Add Sources".
  2. Make sure "Add or create constraints" is selected. Click Next.
  3. Click "Add Files", navigate to your Master XDC, select it, and click Open.
  4. Click Finish.

Once downloaded and included in your project, several changes will need to be made to this XDC file. Start by uncommenting the system clock signal, one button, and all eight signals for one of the Pmod headers. The system clock should be the first signal declared at the top of the file, while the others can be found using Finds (Ctrl-F) for "btn" and "pmod". If your board has a dedicated reset button (for example, "cpu_resetn" on the Nexys Video), use that instead of one of the normal buttons. As for the Pmod header, choose any that isn't intended for input to an onboard XADC. For each of these signals, replace the name in the get_ports call with the name you used in the Block Design, clk, rst, and data, while leaving the bus index alone.

Step 7: Generating the Bit File

In order to generate the .bit file required for programming your Digilent FPGA board, follow the following steps:

  1. In your block design, press F6 or right click anywhere and select "Validate Design". The design should pass validation with no errors if everything was done correctly.
  2. Under the Sources subwindow, select Sources from the tab menu. Right click on your block design and select "Create HDL Wrapper". Make sure the "Let Vivado manage..." option is selected and click OK.
  3. Make sure your new wrapper -- under Design Sources -- is bold in the sources window, if not, right click it and select "Set as Top".
  4. In the Flow Navigator, select "Generate Bitstream" under "Program and Debug". Select Yes when asked whether to launch synthesis and implementation.

Vivado will now automatically generate the file used to program your FPGA. It might take a while, but once done, a window called "Bitstream Generation Completed" will pop up. Select "Open Hardware Manager" and move on to Step 8.

Step 8: Programming the FPGA and Verifying Results

The Digilent Wiki has tutorials for most FPGA boards for how to program a bit file in a variety of ways. The following is going to be the easiest for seeing if our application works. In order to use Vivado's Hardware Manager to program your FPGA, follow these steps:

  1. Plug your board's programming port into a USB port of your PC.
  2. Attach your PmodR2R to whichever Pmod port you tied the lookup table data lines to in your XDC file. In the provided project for Zybo, this is the JE header.
  3. Power your board on, if necessary.
  4. In the Vivado hardware Manager, clock "Open Target" and "Auto Connect".
  5. Click "Program Device" and then then the device number that pops up.
  6. Ensure that the project's bitstream file is selected in the Program Device window. If not, click the "..." to the right of that field, navigate to and select /proj/.runs/impl_1/.bit.
  7. Click Program.

In order to confirm that the project is now working, you can first check status LEDs to see if your board was programmed, then plug your Digilent Analog Discovery (or other oscilloscope) channel one ports into the R2R output ports. I used a six pin male to male connector snapped down to two pins for this, but a couple of small wires will work equally well. Watch the pretty sine wave go!

Step 9: Additional Applications

The following is a list of potential extensions of this project:

  • Adding support for higher frequency waves, even up to the audio range, to control an analog speaker.
  • Adding support for frequency selection, through switch inputs, a lookup table, and making the counter maximum parameter an input bus.
  • Modifying the Python script to generate different types of waves, potentially including smaller amplitudes.
  • Adding several lookup tables with differently shaped wave forms, with data buses multiplexed into the output bus.
  • Adding a processor to the project in order to allow more complex control algorithms (might be able to use an AXI GPIO IP core to provide a simple interface to custom logic).

Each of these ideas come with necessary complications, but could make for interesting projects.

The "Add Module" feature is also very interesting, as it makes it much easier to translate from a functional description to a working prototype. A block diagram makes it much easier to comprehend what a design is actually doing than attempting to understand the same design written in Verilog or VHDL.