# Assignment3: Single-Cycle RISC-V CPU
Contributed by [Ray Huang](https://github.com/coding-ray) (黃柏叡, coding-ray), 2023.
## Set up the Environment
> Primary references of this section:
> 1. System Software Programming & jserv. (2023). [Lab3: Construct a single-cycle RISC-V CPU with Chisel](https://hackmd.io/@sysprog/r1mlr3I7p).
> 2. System Software Programming & jserv. (2023). [sysprog21/ca2023-lab3: Lab3: Construct a single-cycle CPU with Chisel | GitHub](https://github.com/sysprog21/ca2023-lab3).
Operating system: Debian 12.2 (Bookworm)
1. Install Docker. (Ref: [Install Docker Engine on Debian | Docker Docs](https://docs.docker.com/engine/install/debian/))
```bash
# remove all conflicting packages
export CANDIDATES="docker.io docker-doc docker-compose podman-docker containerd runc"; \
for pkg in $CANDIDATES; do \
test ! -z "$(apt list --installed $pkg 2>&1 | sed -n 5p)" && \
sudo apt purge -y --quiet $pkg; \
test $? -ne 0 && \
echo Not installed: $pkg; \
done; \
unset CANDIDATES
# allow apt to use a repository over the HTTPS
sudo apt update && sudo apt install -y ca-certificates curl gnupg
# add Docker’s official GPG key
sudo install -m 0755 -d /etc/apt/keyrings && \
curl -fsSL https://download.docker.com/linux/debian/gpg | \
sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg && \
sudo chmod a+r /etc/apt/keyrings/docker.gpg
# set up the repository
echo \
"deb [arch="$(dpkg --print-architecture)" signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/debian \
"$(. /etc/os-release && echo "$VERSION_CODENAME")" stable" | \
sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
# install the latest Docker engine
sudo apt update && sudo apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
# add the current user to the `docker` group
sudo usermod -aG docker $USER
# activate the changes to groups
newgrp docker
# check Docker version gives no error messages
docker version
```
1. Install GTKWave.
```bash
sudo apt install gtkwave
```
Installed packages: `libjudydebian1 gtkwave`
1. Install [GNU toolchain for RISC-V](https://github.com/xpack-dev-tools/riscv-none-elf-gcc-xpack/releases/latest).
```bash
# download and extract the toolchain
cd /tmp
wget https://github.com/xpack-dev-tools/riscv-none-elf-gcc-xpack/releases/download/v13.2.0-2/xpack-riscv-none-elf-gcc-13.2.0-2-linux-x64.tar.gz
tar zxf xpack-riscv-none-elf-gcc-13.2.0-2-linux-x64.tar.gz
# create a version memo
echo 13.2.0-2 > xpack-riscv-none-elf-gcc-13.2.0-2/version.txt
# move the toolchain to ~/.local/share/, and add it to PATH
mkdir -p ~/.local/share
mv xpack-riscv-none-elf-gcc-13.2.0-2 ~/.local/share/riscv-none-elf-gcc
echo "export PATH=\"\$HOME/.local/share/riscv-none-elf-gcc/bin:\$PATH\"" >> ~/.bashrc
. ~/.bashrc
# make sure the toolchain is installed successfully
riscv-none-elf-gcc -v
# clean up
rm -rf xpack-riscv-none-elf-gcc-13.2.0-2-linux-x64.tar.gz
```
1. For the [Chisel bootcamp](https://github.com/sysprog21/chisel-bootcamp) (a fork of [freechipsproject/chisel-bootcamp:dev](https://github.com/freechipsproject/chisel-bootcamp/tree/dev)) to run or stop. After it has started for 15 seconds (for engine to load), connect to it via http://127.0.0.1:8888/.
```bash
# first run
docker run -d -it --name chisel-bootcamp -p 8888:8888 sysprog21/chisel-bootcamp
# stop with progress saved
docker stop chisel-bootcamp
# later run with progress restored
docker start chisel-bootcamp
```
1. For the [lab 3](https://github.com/coding-ray/2023-ca-lab-3) (a fork of [sysprog21/ca2023-lab3](https://github.com/sysprog21/ca2023-lab3)) to run or stop. Attach to the running container with `docker exec -it ca-lab3 /bin/bash`.
:::warning
:warning: The processes inside the container run as `root` user, with privilege to create files in the current directory (mounted). So, to delete these files owned by `root`, you need to attach to the container first.
:::
```bash
# first run
git clone https://github.com/coding-ray/2023-ca-lab-3 lab3
cd lab3
cp -r ~/.local/share/riscv-none-elf-gcc .
docker build -t ca-lab3 .
docker run -d -it --name ca-lab3 \
--mount type=bind,src="$(pwd)",dst=/app \
ca-lab3
# stop with progress saved
docker stop ca-lab3
# later run with progress restored
docker start ca-lab3
```
## Lab 3: "MyCPU"
In this part, all the waveform is generated by the following command. Get rid of the prefix `WRITE_VCD=1` to run test cases faster. (With the VCD, it takes 25 seconds; without the VCD, it takes 22 seconds on my old PC.)
```shell
WRITE_VCD=1 sbt test
```
### Instruction Fetching Test
In the instruction fetching (IF) part (`src/main/scala/riscv/core/InstructionFetch.scala`), the missing part is to assign the program counter `pc` with one of the following value.
1. "`pc` + 4" if not to branch.
1. "`jump_address_id`" (the address specified by the jump instruction) if to branch.
If the input flag `jump_flag_id` is set, it means "to branch".
In addition, the IF part does the following things.
1. Link `instruction_read_data`, which is the instruction read from memory, to the output signal `instruction`.
1. If `instruction_valid` is not set, `pc` = `pc` implements a stall.
1. Output signal `instruction_address` is always the value of `pc`.
Observations from the following waveform:
1. 0-2 ps: When the processor boots up, the `reset` signal is set (pulled high), so registers (`pc`) initialize with their default value (`pc` = entry address = 0x1000).
1. 0-2 ps: Since `instruction_valid` is not set, `pc` = `pc` implements a stall.
1. 0-2 ps: Before and after the rising edge (triggering) of the clock, input signals (`jump_flag`, `jump_address_id`, `instruction_valid`, `io_instruction`) stay still. It is the setup time (before the triggering) and the hold time (after), to prevent undefined behaviors.
1. 2 ps: At the falling edge of the clock, input signals may change.
1. 3 ps: `instruction_valid` and `jump_flag_id` are set, so `pc` = `jump_address_id` = 0x1000. Although it branches from 0x1000 to 0x1000, it looks like a stall.

Observation from the following waveform: Not to branch (`jump_flag_id` = 0), so `pc` = `pc` + 4 (the instruction width is 4 bytes).

Observation from the moment in the following waveform: To branch, so `pc` = `jump_address_id` =0x1000.

The observations above show that the IF part works as designed, though the output signal `instruction` is always 0 because the memory contains nothing.
### Instruction Decoding Test
In the instruction decoding (ID) part (`src/main/scala/riscv/core/InstructionDecode.scala`), the missing code does the following two things.
1. If the decoded instruction is L-type (`lw lh lb lhu lbu`, whose `opcode` is 0x3), the output flag `memory_read_enable` will be true/1. Otherwise, false/0.
1. If the instruction is S-type (`sw sh sb`, whose `opcode` is 0x23), the output flag `memory_write_enable` will be true. Otherwise, false.
Observations from the following waveform:
1. When the instruction is `sw a0, 4(zero)` (0x00A02223), its lower 7 bits is `opcode` = 0x3, so this instruction is S-type, `memory_write_enable` is true.
1. Seen no instruction having its `opcode` = 0x3, this test doesn't consider L-type instructions. As a result, `memory_read` is always false.

### Instruction Execution Test
In the instruction execution (EXE) part (`src/main/scala/riscv/core/Execute.scala`), the missing code does the following three things.
1. Connect the output `alu_funct` from the ALU control unit (`alu_ctrl`) to the input `funct` of the ALU (`alu`).
1. Set the `op1` of the ALU to the `instruction_address` if it should be (present as `aluop1_source` set high). Otherwise, set it to the content of the source register 1 (`reg1_data`).
1. Set the `op2` of the ALU to the `immediate` if it should be (present as `aluop2_source` set high). Otherwise, set it to the content of the source register 2 (`reg2_data`).
The following waveform shows the case that the `op1` should be `reg1_data` and that `op2` should be `reg2_data`.

The following waveform shows the case that the `op1` should be `instruction_address` and that `op2` should be `immediate`.

### Register Reading and Writing Test

### Byte Loading and Writing Test
For this (byte loading and writing) and the following three tests (quick sorting, 10th Fibonacci number, palindrome checker), the external assembly code is moved to memory by the class `TestTopModule` in the file `src/test/scala/riscv/singlecycle/CPUTest.scala`. It loads the content of its argument, `exeFilename`, in binary to the instruction ROM (`src/main/scala/peripheral/InstructionROM.scala`).
## Incorporate Homework 2 to "MyCPU"
There are some minor changes to the C code in homework 2 to make it work properly and testable in "MyCPU". The code is `csrc/ispalindrome.c`.
1. In the end of function `is_palindrome()`, I change the return values for palindrome and non-palindrome from (1, 0) to (1, 2). Otherwise, since the initial values in the memory of MyCPU are 0, it is ambiguous to have them identical to non-palindrome results.
```clike
if (a == b)
return 1; // palindrome
else
return 2; // not palindrome
```
1. After each `is_palindrome()` returns, I save the result in a separate array. After all `is_palinedrome()` finish, I write the results to a local fixed-size array, which is located in the stack of the program.
1. After all `is_palindrome()` finish, I write the results to the memory located in bytes 4 through 20 (4-20), which is located in the code section of the program.
```clike
for (int i = 1; i <= 4; i++) {
*(volatile int *) (i * 4) = results[i - 1];
}
```
1. If I wrote the result of each `is_palinedrome()` to the code section right after it returns, I would observe that memory located in bytes 8-20 is 0 entirely. I don't know the reason, but I have the workaround above.
1. After the program terminates, I read the results in memory located in bytes 4-20, the results are as expected, which is the first two words being 1 (palindrome), and the later two words being 2 (not palindrome). This test is class `IsPalinedrome` in the file `src/test/scala/riscv/singlecycle/CPUTest.scala`.
The cursor in the first waveform is the moment that the program counter is off 0x1000. It is at around 2.7 ns.

The cursor in the last waveform is the moment that the program returns. It is at around 2716 ns.

Since we know the clock period is 2 ps, we know the program takes around 1,357k clock cycles to finish.
## Miscellaneous Learning Notes
1. Functions in Scala work like functions which are always static inline in C. (Ref. [Lab 3](https://hackmd.io/@sysprog/r1mlr3I7p))
1. Best practices in Chisel development (Ref. [Chisel Best Practices Intensive](https://www.youtube.com/watch?v=e1HRwrNhZhw)):
1. Start from templates.
1. Incorporate tests from the beginning. That is, test-driven development (TDD). (Ref. [Chisel Introduction Intensive](https://www.youtube.com/watch?v=OhMuPQcyynY)).
1. Document your code. (With ScalaDoc tools, GitHub wiki, etc.)
1. Follow coding styles. Use style guidelines.
1. Have a code management strategy. (With git, branch early and often.)
1. Don't repeat yourself. Utilize functions, objects and classes.
1. Do more than you think you can in Scala. Notice the difference between Scala and Chisel.
1. Use the collection library (featured in Scala).
1. Use an IDE.
1. Share your knowledge.
1. Have your own Chisel support team.