This is a RISC-V CPU, that I build to meet the 32I base integer instruction set. The source is available on my GitHub. It’s built with SystemVerilog and verified with pyuvm and cocotb. I followed the Building a RISC-V CPU Core course by the Linux foundation to get an idea of what order I should be implementing features in. However I used SystemVerilog instead of TL-Verilog and implemented all the verification myself, as verification was not part of the course.

Overview

The core has 5 modules and 2 interfaces.

Top level logic diagram of CPU

Instruction Decoder

The instruction decoder issues control signals to each of the other modules based on the current instruction.

Program Counter

The program counter fetches the next instruction from instruction memory. Normally it just fetchs the instruction from the current address + 4. If the instruction decoder issues a jump or branch the program counter will update the store address based on that signal. This may mean jumping to a specific address set by the register input or relative to the current address based on the immediate input.

Registers

There are 32 registers which are each 32-bits wide. They are used to temporarily store data in the CPU. The CPU can write to one and read from two registers simultaneously. The register can be written to by the program counter, the arithmetic logic unit, the data memory, or with the immediate value from the instruction based on the control signals from the instruction decoder.

Arithmetic Logic Unit

The arithmetic logic unit performs an arithmetic function (such as an add or subtract) based on the control signals from the instruction decoder. The function is performed on either the immediate value and the second read port of the registers or on both read ports from the registers. The result is written back to the write port of the registers.

Branch Decider

The branch decider issues a branch command to the program counter if the conditions set by the control signals are met. For example, if the value on the first register read port is less than on the second read port and the less than control is issued.

Instruction and Data Memory Interface

The core has two interfaces; one for fetching instructions, and the other for loading and storing data to and from main memory. In testing, pyuvm drives the inputs to these interface based on the addresses they output and the control signals.

Testing

Each of the modules are tested independently using pyuvm and I used cocotb coverage to ensure coverage. I haven’t done any full integration testing on the top level. I have just written a simple program to test each of the commands work as expected and inspected the waves visually. And I wrote a program to calculate the largest 32-bit fibonacci number to see the core working.

Assembly

In order to test the top level and not lose my mind writing machine code, I wrote an assembler in python. It supports all of the RV32I instructions, setting labels, setting instruction addresses, and binary, octal, decimal, and hexadecimal numbers. I happened to be watching Fabian Schuiki’s 8-bit superscalar breadboard computer series while I was making this and his assembler looked very clean, so mine is heavily inspired by his.

The Future

I plan on trying to get the core working on an FPGA and also to improve the top level testing. I will hopefully try to build a superscalar CPU in the future and to implement some of the RISC-V extensions. But for now I am happy with the state of the project and feel I’ve learned what I hoped I would.