RISC-V on a Basys 3 FPGA Development Board
Introduction
I’ve been working on learning how to program/configure FPGAs. I got off to a rocky start due to US export regulations, but after that I’ve been making good progress. Two good books I found are:
- “Digital Design and Computer Architecture: RISC-V Edition” by Sarah L. Harris and David Harris.
- “FPGA Programming for Beginners” by Frank Bruno.
The Harris’s RISC-V book is an excellent source on how to create a RISC-V CPU core in a Hardware Definition Language (HDL) giving examples in both System Verilog and VHDL. The Bruno book is great for how to implement simple hardware projects on Digilent FPGA boards based on the Artix-7 FPGA architecture.
This blog post looks at taking a RISC-V core from the Harris’s book and implementing it on a Digilent Basys3 FPGA development board using the information from Bruno’s book.
Getting RISC-V Up and Running
The Harris’s book has a lot of information on designing a RISC-V CPU. This implementation executes one instruction per clock cycle and implements the RISC-V instructions:
- lw, sw – load/save a word from/to memory
- add, sub, and, or, slt, addi, andi, ori, slti – arithmetic/logic ALU instructions
- beq – conditional branch
- jal – jump and link.
These are enough instructions to execute simple programs and really emphasizes the R in RISC (really reduced instruction set).
First, I took the source code from the Harris’s book and along with the testbench and input it into Vivado. There were a couple of typos in the text and some of the code was left as an exercise, however the online book resources contained the complete correct code. With this in hand I was able to run the testbench in the Vivado simulator and have it pass the single automated test.
I was pretty happy at this, I could execute RISC-V instructions and examine the circuit schematic produced.

I then went to run it on the Basys3 board. This didn’t compile, complaining there weren’t enough I/O ports on the board. The reason for this is that the RISC-V core takes as input both the instruction and data memories as input parameters. These were instantiated as part of the testbench, and any input parameters that aren’t created in the circuit are expected to be connected to external ports.
Creating a SoC
My next step was to create a System on a Chip (SoC) module that would instantiate some memory, instantiate the RISC-V core and provide connections to the Digilent board’s I/O ports.
First I added the instruction and data memory to the SoC module and when I got it to compile, I got an error that nothing was produced. This turned out to be because I didn’t have any outputs, the optimizer was able to remove every component resulting in an empty implementation. The easiest way past this was to connect a couple of things to the LEDs on the board, this way I could see the same results as the test in the testbench program, namely that the program ends with DataAdr == 100 and WriteData == 25.
This causes Vivado to actually generate some circuits, however it then complains it can’t find a layout that works at 100MHz. The solution is given in Bruno’s book to include a clock module from the IP section of Vivado. This clock module is free, easily downloaded and incorporated into your project. With this you can configure the clock to a slower speed to allow more to happen in one clock cycle. Configuring the clock to 50MHz allowed Vivado to produce a layout and then generate a bitstream that I could download to the Basys board. This then lit up the LEDs as expected. The source code for the SoC module is at the end of this article.

Where to Next
There is still a long way to go to create a general purpose computer. The Assembly Language program is hard coded into the circuitry of the FPGA. There is very little I/O with only using a switch for reset and then the LEDs to give a limited view of the address bus.
Further the Vivado optimizer can remove a lot of circuitry because not all 32-bits are used in this example and it can execute the program in the optimizer and hard code the result. To start to make this useful it would be nice to:
- Add memory mapped I/O for more devices on the Basys board. Such as outputting numbers to the four seven-segment displays.
- Add more instructions from the RISC-V instruction set.
- Provide a way to download the machine code Assembly Language to the processor as it’s running. I.e. hard code a boot loader rather than the actual program.
- Add support for a VGA monitor, as the Basys does have a VGA video port. Similarly figure out a way to connect a keyboard.
The Basys 3 has rather limited memory, since it only has its distributed memory spread over various LUTs. More expensive boards from Digilent include banks of RAM that could be used to hold more extensive programs. I suspect if I wanted to port a BASIC interpreter, I would need the next Digilent board up in the line. But I have a lot of work to do before getting to that point.
Summary
Creating a small CPU on an FPGA is certainly fun, but it shows the amount of work required to create a CPU and its ecosystem. This is a simple CPU with a few instructions, no pipeline and no caches. But at least it is 32-bit. Implementing some of the RISC-V instruction set, which means I can use the GCC Assembler running on a Raspberry Pi to create the machine code. It is a long way from having what is required to run the Linux Kernel. But for those interested in minimalistic computing this is another alternative to creating custom CPUs that is a bit easier than constructing one out of discrete TTL logic components.
`timescale 1ns/10ps
module soc(
input wire clk,
input wire reset,
output logic [15:0] led
);
logic [31:0] WriteData, DataAdr;
logic MemWrite;
logic [31:0] PC, Instr, ReadData;
logic clk_50;
generate
sys_pll u_sys_pll
(
.clk_in1 (clk),
.clk_out1 (clk_50)
);
endgenerate
// instantiate processor and memories
riscvsingle rvsingle( clk_50, reset, PC, Instr, MemWrite,
DataAdr, WriteData, ReadData);
socimem imem(PC, Instr);
socdmem dmem(clk_50, MemWrite, DataAdr, WriteData, ReadData);
always @(negedge clk)
begin
if(MemWrite) begin
led[7:0] = WriteData[7:0];
led[15:8] = DataAdr[7:0];
end
end
endmodule
module socimem(input logic [31:0] a, output logic [31:0] rd); logic [31:0] RAM[63:0]; initial begin RAM[0] = 32'h00500113; RAM[1] = 32'h00C00193; RAM[2] = 32'hFF718393; RAM[3] = 32'h0023E233; RAM[4] = 32'h0041F2B3; RAM[5] = 32'h004282B3 RAM[6] = 32'h02728863; RAM[7] = 32'h0041A233; RAM[8] = 32'h00020463; RAM[9] = 32'h00000293; RAM[10] = 32'h0023A233; RAM[11] = 32'h005203B3; RAM[12] = 32'h402383B3; RAM[13] = 32'h0471AA23; RAM[14] = 32'h06002103; RAM[15] = 32'h005104B3; RAM[16] = 32'h008001EF; RAM[17] = 32'h00100113; RAM[18] = 32'h00910133; RAM[19] = 32'h0221A023; RAM[20] = 32'h00210063; end assign rd = RAM[a[31:2]]; // word aligned endmodule
module socdmem(input logic clk, we,
input logic [31:0] a, wd,
output logic [31:0] rd);
logic [31:0] RAM[63:0];
assign rd = RAM[a[31:2]]; // word aligned
always_ff @(posedge clk)
if (we) RAM[a[31:2]] <= wd;
endmodule
[…] Introduction I’ve been working on learning how to program/configure FPGAs. I got off to a rocky start due to US export regulations, but after that I’ve been making good progress. Two good books I found are: “Digital Design and Computer Architecture: RISC-V Edition” by Sarah L. […]
RISC-V on a Basys 3 FPGA Development Board | Ra...
February 22, 2023 at 9:40 pm
[…] Last time, I talked about getting a minimal RISC-V CPU up and running on my Basys FPGA development board. In this article, we’ll connect up the seven segment displays as memory mapped I/O and write a simple RISC-V Assembly Language program to count on the display. […]
Counting on my RISC-V FPGA CPU | Stephen Smith's Blog
March 8, 2023 at 6:41 pm