~ruther/verilog-riscv-semestral-project

b0f8702877121832dfdee7d921af417237673284 — Rutherther 1 year, 4 months ago 6051795
docs: add basic documentation
3 files changed, 198 insertions(+), 5 deletions(-)

A README.md
A tests/README.md
M tests/run.py
A README.md => README.md +78 -0
@@ 0,0 1,78 @@
# RISC-V single cycle processor in SystemVerilog
Available at https://github.com/Rutherther/verilog-riscv-semestral-project

This repository contains RISC-V processor written in SystemVerilog.

## Architecture

## Requirements
- make
- verilator
- riscv32-unknown-elf toolchain (binutils, gcc)
- python3

There is a flake file for dev shell with all required dependencies
including gtkwave for viewing vcd files.

## Testing
There are two separate testing mechanisms used,
one is using Verilog testbenches. These testbenches
are not automatically evaluated, but meant for observation
of waves.

Then there are ISA tests, official ones and few custom tests
as well. These may be ran from Python script and validate correct
functionality automatically.

### Verilog testbenches
All testbenches are located in `testbench/` folder.
These testbenches do not assert anything by themselves.
They pass simple arguments to the modules.

#### Individual modules
To run a module `$tb_module` (produce wave file),
use `make ./waves/$tb_module.vcd`.
To show this file, run
`make show MODULE=$tb_module`

#### Running programs
There is a special testbench for running programs
called `tb_cpu_program`.
It is used in the ISA tests as well.

To run a program, use `make run_program`.
By default, program for finding gcd will be ran,
with zeros in memory. (it expects
the numbers in memory, so it won't do much)

The target expects some arguments, namely
- `PROGRAM` - name of the program to run
- `MEMORY_LOAD` - path to the file to load memory from. Expected format is Verilog hex format. Do note that ram consists of 32 bit elements.
- `MEMORY_WRITE` - path to the file to write memory to. By default `out/program_$PROGRAM_memory_out.dat`, so doesn't have to be changed

For example, `make run_program PROGRAM=gcd MEMORY_LOAD=tests/custom/gcd/1071-462-21-input.dat`.
This will run gcd program with 1071 and 462 as arguments. Expected output is 21.
At the end of the program, registers are dumped to stdout. The value is in register 14 as well
as in the memory on address zero.

The program is expected to be at `programs/$PROGRAM.c`.
It is expected that it has a main function, no arguments are passed to main.
Main will be called from `programs/start.S`.

### Compiling C programs
Programs are compiled automatically when running
programs or tests.

To produce an object file for $program,
use `make ./programs/bin/start-$program.o`.
The file produced is the C program file linked with `./programs/start.S`
This file might be used for `objdump`.
There is make target for showing objdumps
`make objdump PROGRAM=$program`.

To produce elf file, use `make ./programs/bin/$program.bin`.
To produce a file that may be loaded from verilog with `readmemh`,
use `make ./programs/bin/$program.dat`.

### ISA tests
ISA tests are documented in [tests/README.md](tests/README.md)

A tests/README.md => tests/README.md +111 -0
@@ 0,0 1,111 @@
# RISC-V tests
Under the hood, the tests use `tb_cpu_program` Verilog testbench.

Python script called `run.py` is used for testing.
This will use makefiles under the hood,
one from the project root, and other from `tests/official` subdirectory.

## Test types
There are two types of tests,
official riscv tests that work by selftesting
the processor. That means that if there is a problem
with more instructions like branches, store words,
it's possible that the tests will pass even though
the processor is not working properly.

The other type are custom tests. These do not depend
on functionality of processor instructions.
There is just a few of these. For each program,
it's possible to specify different memory to load,
so multiple parameters are passed to the same program.

## Test success validation
To validate test success, memory is dumped to a file
at the end of the test - upon getting to ebreak instruction.
This memory is then validated against file with expected memory
values. The memory file format is Verilogs hex format.

Selftests are checked the same way, but upon success, on address
zero, there should be 0xAA. On fail, 0xFF.
Here are the macros used for test pass and fail
``` assembly
#define RVTEST_PASS                                                     \
        addi x1, zero, 0xAA;                                            \
        sw x1, 0(zero);                                                 \
        nop;                                                            \
        ebreak;

#define TESTNUM gp
#define RVTEST_FAIL                                                     \
        addi x1, zero, 0xFF;                                            \
        sw x1, 0(zero);                                                 \
        sw x1, 4(TESTNUM);                                              \
        nop;                                                            \
        ebreak;
```
These macros are defined in `tests/official/env/p/riscv_test.h`.

## Usage
To run the tests, `run.py` can be used.
By default, it will run the custom tests.

It must be supplied one of run or list subcommands.
To list all the tests, use `./run.py list`. To run the tests,
supply `./run.py run`.

Every test has a group. For custom tests the group specifies the program to run.
Name of the test specifies the arguments.
For official tests, there is only one group so far, `rv32ui`.

It's possible to filter one or more of the tests,
by using `--filter` flag. For example `./run.py run --filter gcd`
to run only tests from the gcd group. For running only one test
from the group, specify it after a dot. For example `./run.py --filter gcd.1071-462-21`.

```
usage: run.py [-h] [-f [FILTER ...]] [-t {custom,official}] [--trace] [--print-registers] {run,list}

Test simple RISC-V processor written in Verilog.

positional arguments:
  {run,list}            What to do. Either run the test, or run testcases based on filter.

options:
  -h, --help            show this help message and exit
  -f [FILTER ...], --filter [FILTER ...]
                        filter, should be in group.test format
  -t {custom,official}, --type {custom,official}
                        type of the testcases, either custom testcases or official riscv selftests
  --trace               trace, produce vcd file
  --print-registers     dump registers on ebreak to stdout
```

## Creating custom tests
To create a custom test, two things are needed.

First, a C program has to be made. This program is expected to be located in
`../programs/`. It should have a `main` function. This function will be called.
It should ideally load parameters from the beginning of the memory, and save
the result to the memory afterwards, again, at the beginning of the memory.

Second, arguments have to be specified. To do this, first create a directory
`custom/$program`. In this directory, files with parameters should be placed.
For each test, two files are needed, input memory and expected memory.

Name of the files should be `$test-input.dat` and `$test-expected.dat`.
They should be in Verilog hex format (input will be read by readmemh, expected
data will be compared against file written with writememh). Only first few
words have to be specified, no need to put in full memory image.

I have chosen to name the files by parameters passed in, every parameter
separated by a dash. And at the end, the expected result is listed.
For example, gcd.1071-462-21 will pass in 1071 and 462 as arguments
to the gcd program. The expected result (gcd(1071, 462)) is 21.

## Existing custom test programs (groups)
- gcd
  - Calculates gcd of two numbers
- branches
  - Tries all branch instructions
- memory_bytes
  - Tries to read and write bytes to memory

M tests/run.py => tests/run.py +9 -5
@@ 127,7 127,11 @@ def filter_tests(groups: list[TestGroup], group_name: str|None, test_name: str|N
    return groups

# Program
parser = argparse.ArgumentParser("Test simple RISC-V processor written in Verilog.")
parser = argparse.ArgumentParser(
    prog = "run.py",
    description = "Test simple RISC-V processor written in Verilog."
)

parser.add_argument(
    "command",
    choices = [ "run", "list"],


@@ 138,24 142,24 @@ parser.add_argument(
    "--filter",
    type = str,
    nargs = "*",
    help = "Filter, should be in group.test format."
    help = "filter, should be in group.test format"
)
parser.add_argument(
    "-t",
    "--type",
    choices = ["custom", "official"],
    default = "custom",
    help = "Type of the testcases, either custom testcases or official riscv selftests.",
    help = "type of the testcases, either custom testcases or official riscv selftests",
)
parser.add_argument(
    "--trace",
    action = "store_true",
    help = "Trace, produce vcd file",
    help = "trace, produce vcd file",
)
parser.add_argument(
    "--print-registers",
    action = "store_true",
    help = "Trace, produce vcd file",
    help = "dump registers on ebreak to stdout",
)
# parser.add_argument(
#     "--print-memory",

Do not follow this link