RISC-V CPU & CUSTOM GPU ON AN FPGA PART 4 – SETTING UP THE FPGA BOARD AND OUR TEST DEVICE
Welcome to part 4 of the series.
Before we continue, if you haven’t done so, I recommend going back to the first part and syncing up and installing the tools required for the rest of the series.
Getting started: Vivado and Board Files
Even though you have access to the final live project files on github, it’s still useful to learn how to set up an empty project for the smaller code items we’ll be going over in this part.
Before we start though, we’ll need to do one more thing: Head over to Digilent’s Board Files Repository, sync up the proper board folders matching your board (if not A7-100T) and move them to your Xilinx install boards folder, which is somewhere similar to this path: <YourInstallPath>/Xilinx/Vivado/2020.2/data/boards
After this, we can start the Vidado software, which will present us with a start screen similar to this:
From here, we have two choices. If you wish to directly jump to the end of this adventure, you can simply choose Open Project, navigate to the NekoIchi source files that you’ve synced to in the first part of this series, and Synthesize / Implement those, then upload to your board and jump to the later tutorial sections. However, if you’re the less adventurous type, and wish to see all the steps, read on.
Creating our project
If you have opted to follow along the step-by-step, first we’ll need to pick Create Project from the above splash screen in Vivado.
Step 1: Name your project to something meaningful (I’ll be using nekotutorial), and choose a project directory.
Step 2: Choose RTL Project and make sure you have Do not specify sources at this time selected.
Step 3: Click the Boards tab, and scroll to select your board. I’m using an Arty A7-100 board which I recommend for compatibility reasons thought this series, but any other reasonably sized board should also work with minor tweaks.
Adding the Top Module File
And now we have our project created for us, though it lacks any source files.
Let’s add one by using this handy plus icon at the top of the Sources panel:
From here, select Add or create design sources, Next, and Create File. From the new popup window, choose SystemVerilog and give it a name, for instance, nekotop (since this is the top level module), and select Finish, choosing OK for further dialogs that might as you if you wish to continue.
Our new file will now appear under Design Sources in the Sources panel:
As you can see, there isn’t much going on here except a default boilerplate code which is an empty module.
The top module is usually the one that interfaces to the outside world using FPGA wiring, such as input clocks, buttons and other devices such as video and/or storage devices, and sometimes extra onboard memory modules such as DDR3.
Board Files and Constraints
For our first task, we’ll start by wiring our clock and reset button, and one of the onboard LEDs. However, to do that, we’ll need a file that describes the ‘constraints’ of the board, which is the pin in/outs and their voltage levels as well as any clock properties.
You can find this file in the NekoIchi git page for Arty A7-100 board, but for our short test we’ll add one manually.
Head over to that Plus icon in the Sources panel again and click it, this time selecting Add or creaste constraints, and hit Next. Use Create File again, making sure the displayed file type is XDC, and name it something short, ike a7const and hit Finish. You’ll notice that this file now appears under Constraints folder in your Sources panel.
NOTE: The full set of constraint files (XDC files) for your Digilent board are always available at the github board repository, if you wish to simply copy one of those to your project instead.
At this point, go ahead and double-click the a7const.xdc file to open it, and paste our few constraints to it:
## Clock signal
set_property -dict { PACKAGE_PIN E3 IOSTANDARD LVCMOS33 } [get_ports { CLK_I }]; #IO_L12P_T1_MRCC_35 Sch=gclk[100]
create_clock -add -name sys_clk_pin -period 10.00 -waveform {0 5} [get_ports { CLK_I }];
##Buttons
set_property -dict { PACKAGE_PIN D9 IOSTANDARD LVCMOS33 } [get_ports { RST_I }]; #IO_L6N_T0_VREF_16 Sch=btn[0]
##LEDs
set_property -dict { PACKAGE_PIN H5 IOSTANDARD LVCMOS33 } [get_ports { led[0] }]; #IO_L24N_T3_35 Sch=led[4]
set_property -dict { PACKAGE_PIN J5 IOSTANDARD LVCMOS33 } [get_ports { led[1] }]; #IO_25_35 Sch=led[5]
set_property -dict { PACKAGE_PIN T9 IOSTANDARD LVCMOS33 } [get_ports { led[2] }]; #IO_L24P_T3_A01_D17_14 Sch=led[6]
set_property -dict { PACKAGE_PIN T10 IOSTANDARD LVCMOS33 } [get_ports { led[3] }]; #IO_L24N_T3_A00_D16_14 Sch=led[7]
## Configuration options, can be used for all designs
set_property BITSTREAM.CONFIG.CONFIGRATE 50 [current_design]
set_property CONFIG_VOLTAGE 3.3 [current_design]
set_property CFGBVS VCCO [current_design]
set_property BITSTREAM.CONFIG.SPI_BUSWIDTH 4 [current_design]
set_property CONFIG_MODE SPIx4 [current_design]
This will give us a button, an input clock, 4 LEDs and device configuration settings for the A7-100 board. If you have a different board, make sure to copy the matching settings from the above github repository instead.
Connecting to the outside world
Now that we have our peripheral connections declared, we can use them in our design by denoting their data flow direction, which can be either of input, output or inout. It’s pretty straightforward in the case of switches and clocks, and signals are usually input or output, but for more complex bus layouts we might want to use inout in which case devices on both ends of that wire can either send or receive signals.
Double-click to open our nekotop.sv file (our top module), and modify it as follows:
`timescale 1ns / 1ps
module nekotop(
// Input clock
input CLK_I,
// Reset on lower panel, rightmost button
input RST_I,
// 4 monochrome LEDs
output [3:0] led
);
endmodule
You’ll notice that we’re using the bit-order syntax for the led entry in our input/output port list, as we saw earlier when talking about the register file. This kind of grouping forms a ‘bus’ of signals and in will show up as a group in simulations, instead of a single signal.
But this alone isn’t sufficient to see something interesting. Let’s add an LED that blinks on and off to get familiar with clocks.
`timescale 1ns / 1ps
module nekotop(
// Input clock
input CLK_I,
// Reset on lower panel, rightmost button
input RST_I,
// 4 monochrome LEDs
output [3:0] led
);
// 32 bit counter, set to 32 bit decimal zero initially
logic [31:0] counter = 32'd0;
// At every rising (pos) edge of input clock...
always @(posedge CLK_I) begin
// Increment counter by one, visible on the next clock
counter <= counter + 32'd1;
end
// Continuous assignment of bit 26 of counter to led[3], and 3 bits of zero to led[2], led[1], led[0]
assign led = { counter[26], 3'b000 };
endmodule
The above circuit counts at 100Mhz (which is the input clock we have on Arty A7-100 board), and will set the on/off state of the led on the leftmost side of our board (led[3]) to match the value of the 26th bit of our counter. This causes a slow flashing LED.
To see it in action, we’ll need to use the Synthesize, Implement, and finally Generate Bitstream steps in the Project manager. If you want to take a shortcut, simply click the Generate Bitstream button and wait a minute while Vivado does its thing.
If everything went well, after a short wait you’ll be presented with this choice:
From here, choose Open Hardware Manager. Now would be a good time to connect your board to the PC with a USB cable (and not a power adapter at the same time! I’ve done that mistake and almost killed my board. Make sure you only use the USB cable!)
When the Hardware Manager starts up, click the Auto Connect button which looks like a small plug icon, and your board should show up in the list. Right click on the device name (xc7a100t for the A7-100 board) and select Program Device…
This will take you to a dialog to select the programming file we just generated (the Bitstream file). This will, by default, end up in your project folder, under the .runs folder with a path similar to nekotutorial/nekotutorial.runs/impl_1/nekotop.bit. Navigate to this file using the Bitstream file navigation button (the ellipsis icon) and choose it from the file dialog box, then hit the Program button.
At this point, the board will be programmed via the USB/UART connector and the leftmost green LED on bottom left corner of your board should start slowly blinking.
This concludes part 4, Now that we know our board works, it’s time to start adding some other devices and do something interesting with it.