The projects are part of your design project worth 2 credit points. As such they run in parallel to the actual course. So be aware that the due date for project and homework might be very close to each other! Start early and do not procrastinate.
We hope this project will enhance your C programming skills, familiarize you with some of the details of RISC-V, and prepare you for what's to come later in this course.
In this project, you will create an emulator that is able to execute a subset of the RISC-V ISA. You'll provide the machinery to decode and execute a couple dozen RISC-V instructions. You're creating what is effectively a miniature version of VENUS!
The RISC-V green card provides some information necessary for completing this project.
Make sure you read through the entire specification before starting the project.
The whole project is split into two parts. Project 1.1 (Part 1) and Project 1.2 (Part 2). Those will be autograded separately and have their own deadlines. You start with Project 1.1/ Part 1 now - but the instructions for Project 1.2 are also given alreay.
You will be using gitlab to collaborate with your group partner. Autolab will use the files from gitlab. Make sure that you have access to gitlab. In the group CS110_Projects you should have access to your project 1 project. Also, in the group CS110, you should have access to the p1_framework.
You have to work at this project as a team. We invite you to use all of the features of gitlab for your project, for example branches, issues, wiki, milestones, etc.
We require you to push very frequently to gitlab. In your commits we want to see how the code evolved. We do NOT want to see the working code suddenly appear - this will make us suspicious.
We also require that all group members do substantial contributions to the project. This also means that one group member should not finish the project all by himself, but distribute the work among all group members!
Gitlab has excellent tools to track that (see "Repository : Contributors"). At the end of Project 1 we will interview all group members and discuss their contributions, to see if we need to modify the score for certain group members.
The files you will need to modify:
You should definitely consult through the following, thoroughly:
You should not need to look at these files, but here they are anyway:
When your project is done, please submit all the code including the framework to your remote GitLab repo by running the following commands.
$ git commit -a
$ git push origin master:master
You will NOT be submitting extra files. If you add a public helper functions, please place the function prototypes in utils.h. Besides, we only grade code on the master branch. If you do not follow these requirements, your code will likely not compile and you will get a zero on the project.
The files provided in the start kit comprise a framework for a RISC-V emulator. You'll first add code to part1.c and utils.c to print out the human-readable disassembly corresponding to the instruction's machine code. Next, you'll complete the program by adding code to part2.c to execute each instruction (including perform memory accesses). Your simulator must be able to handle the machine code versions of the following RISC-V machine instructions. We've already given you a framework for what cases of instruction types you should be handling.
It is critical that you read and understand the definitions in types.h before starting the project. If they look mysterious, consult chapter 6 of K&R, which covers structs, bitfields, and unions. Check yourself: why does sizeof(Instruction) == 4?
The instruction set that your emulator must handle is listed below. All of the information here is copied from the RISC-V green sheet for your convenience; you may still use the green card as a reference.
Instruction | Type | Opcode | Funct3 | Funct7/IMM | Operation |
add rd, rs1, rs2 | R | 0x33 | 0x0 | 0x00 | R[rd] ← R[rs1] + R[rs2] |
mul rd, rs1, rs2 | 0x0 | 0x01 | R[rd] ← (R[rs1] * R[rs2])[31:0] | ||
sub rd, rs1, rs2 | 0x0 | 0x20 | R[rd] ← R[rs1] - R[rs2] | ||
sll rd, rs1, rs2 | 0x1 | 0x00 | R[rd] ← R[rs1] << R[rs2] | ||
mulh rd, rs1, rs2 | 0x1 | 0x01 | R[rd] ← (R[rs1] * R[rs2])[63:32] | ||
slt rd, rs1, rs2 | 0x2 | 0x00 | R[rd] ← (R[rs1] < R[rs2]) ? 1 : 0 | ||
sltu rd, rs1, rs2 | 0x3 | 0x00 | R[rd] ← (U(R[rs1]) < U(R[rs2])) ? 1 : 0 | ||
xor rd, rs1, rs2 | 0x4 | 0x00 | R[rd] ← R[rs1] ^ R[rs2] | ||
div rd, rs1, rs2 | 0x4 | 0x01 | R[rd] ← R[rs1] / R[rs2] | ||
srl rd, rs1, rs2 | 0x5 | 0x00 | R[rd] ← R[rs1] >> R[rs2] | ||
sra rd, rs1, rs2 | 0x5 | 0x20 | R[rd] ← R[rs1] >> R[rs2] | ||
or rd, rs1, rs2 | 0x6 | 0x00 | R[rd] ← R[rs1] | R[rs2] | ||
rem rd, rs1, rs2 | 0x6 | 0x01 | R[rd] ← (R[rs1] % R[rs2] | ||
and rd, rs1, rs2 | 0x7 | 0x00 | R[rd] ← R[rs1] & R[rs2] | ||
lb rd, offset(rs1) | I | 0x03 | 0x0 | R[rd] ← SignExt(Mem(R[rs1] + offset, byte)) | |
lh rd, offset(rs1) | 0x1 | R[rd] ← SignExt(Mem(R[rs1] + offset, half)) | |||
lw rd, offset(rs1) | 0x2 | R[rd] ← Mem(R[rs1] + offset, word) | |||
lbu rd, offset(rs1) | 0x4 | R[rd] ← U(Mem(R[rs1] + offset, byte)) | |||
lhu rd, offset(rs1) | 0x5 | R[rd] ← U(Mem(R[rs1] + offset, half)) | |||
addi rd, rs1, imm | 0x13 | 0x0 | R[rd] ← R[rs1] + imm | ||
slli rd, rs1, imm | 0x1 | 0x00 | R[rd] ← R[rs1] << imm | ||
slti rd, rs1, imm | 0x2 | R[rd] ← (R[rs1] < imm) ? 1 : 0 | |||
sltiu rd, rs1, imm | 0x3 | R[rd] ← (U(R[rs1]) < U(imm)) ? 1 : 0 | |||
xori rd, rs1, imm | 0x4 | R[rd] ← R[rs1] ^ imm | |||
srli rd, rs1, imm | 0x5 | 0x00 | R[rd] ← R[rs1] >> imm | ||
srai rd, rs1, imm | 0x5 | 0x20 | R[rd] ← R[rs1] >> imm | ||
ori rd, rs1, imm | 0x6 | R[rd] ← R[rs1] | imm | |||
andi rd, rs1, imm | 0x7 | R[rd] ← R[rs1] & imm | |||
jalr rd, rs1, imm | 0x67 | 0x0 |
R[rd] ← PC + 4
PC ← R[rs1] + imm |
||
ecall | 0x73 | 0x0 | 0x000 |
(Transfers control to operating system)
a0 = 1 is print value of a1 as an integer. a0 = 4 is print the string at address a1. a0 = 10 is exit or end of code indicator. a0 = 11 is print value of a1 as a character. |
|
sb rs2, offset(rs1) | S | 0x23 | 0x0 | Mem(R[rs1] + offset) ← R[rs2][7:0] | |
sh rs2, offset(rs1) | 0x1 | Mem(R[rs1] + offset) ← R[rs2][15:0] | |||
sw rs2, offset(rs1) | 0x2 | Mem(R[rs1] + offset) ← R[rs2] | |||
beq rs1, rs2, offset | SB | 0x63 | 0x0 |
if(R[rs1] == R[rs2])
PC ← PC + {offset, 1b'0} |
|
bne rs1, rs2, offset | 0x1 |
if(R[rs1] != R[rs2])
PC ← PC + {offset, 1b'0} |
|||
blt rs1, rs2, offset | 0x4 |
if(R[rs1] < R[rs2])
PC ← PC + {offset, 1b'0} |
|||
bge rs1, rs2, offset | 0x5 |
if(R[rs1] >= R[rs2])
PC ← PC + {offset, 1b'0} |
|||
bltu rs1, rs2, offset | 0x6 |
if(U(R[rs1]) < U(R[rs2]))
PC ← PC + {offset, 1b'0} |
|||
bgeu rs1, rs2, offset | 0x7 |
if(U(R[rs1]) >= U(R[rs2]))
PC ← PC + {offset, 1b'0} |
|||
auipc rd, offset | U | 0x17 | R[rd] ← PC + {offset, 12b'0} | ||
lui rd, offset | 0x37 | R[rd] ← {offset, 12b'0} | |||
jal rd, imm | UJ | 0x6f |
R[rd] ← PC + 4
PC ← PC + {imm, 1b'0} |
For further reference, here are the bit lengths of the instruction components.
R-TYPE | funct7 | rs2 | rs1 | funct3 | rd | opcode |
Bits | 7 | 5 | 5 | 3 | 5 | 7 |
I-TYPE | imm[11:0] | rs1 | funct3 | rd | opcode |
Bits | 12 | 5 | 3 | 5 | 7 |
S-TYPE | imm[11:5] | rs2 | rs1 | funct3 | imm[4:0] | opcode |
Bits | 7 | 5 | 5 | 3 | 5 | 7 |
SB-TYPE | imm[12] | imm[10:5] | rs2 | rs1 | funct3 | imm[4:1] | imm[11] | opcode |
Bits | 1 | 6 | 5 | 5 | 3 | 4 | 1 | 7 |
U-TYPE | imm[31:12] | rd | opcode |
Bits | 20 | 5 | 7 |
UJ-TYPE | imm[20] | imm[10:1] | imm[11] | imm[19:12] | rd | opcode |
Bits | 1 | 10 | 1 | 8 | 5 | 7 |
Just like the regular RISC-V architecture, the RISC-V system you're implementing is little-endian. This means that when given a value comprised of multiple bytes, the least-significant byte is stored at the lowest address. If needed, review the lecture 4 slides 35-38.
The framework code we've provided operates by doing the following.
The framework supports a handful of command-line options:
In part 2, you will be implementing the following:
Many UNUSED modifiers, which you may find in the declaration of functions, are added to suppress unused-variable-warnings. You can remove them immediately you finish the code.
By the time you're finished, they should handle all of the instructions in the table above.
The goal of this part is, when given an instruction encoded as a 32-bit integer, to reproduce the original RISC-V instruction in human-readable format. For this part, you will not be referring to registers by name; instead, you should refer to registers by their numbers (as defined on the RISC-V Green Card). Please look at the constants defined in utils.h when printing the instructions. More details about the requirements are below.
To implement this functionality, you will be completing the following:
You may run the disassembly test by typing in the following command. If you pass the tests, you will see the output listed here.
$ make part1
gcc -g -Wall -Werror -Wfatal-errors -O2 -o riscv utils.c part1.c part2.c riscv.c
simple_disasm TEST PASSED!
multiply_disasm TEST PASSED!
random_disasm TEST PASSED!
---------Disassembly Tests Complete---------
The tests provided do not test every single possiblity, so creating your own tests to check for edge cases is vital. If you would like to only run one specific test, you can run the following command:
$ make [test_name]_disasm
To create your own tests, you first need to create the relevant machine code. This can either be done by hand or by using the Venus simulator. You should put the machine instructions in a file named [test_name].input and place that file inside riscvcode/code. Then, create what the output file will look like [test_name].solution and put this output file in riscvcode/ref. See the provided tests for examples on these files. To integrate your tests with the make command, you must modify the Makefile. On Line 4 of the Makefile, where it says ASM_TESTS, add [test_name] to the list with spaces in between file names.
To run your code through cgdb, you can compile your code using make riscv. Then you can run the debugger on the riscv executable. You will need to the supply input file as a command-line argument within the debugger.
If your disassembly does not match the output, you will get the difference between the reference output and your output. Make sure you at least pass this test before submitting part1.c.
For this part, only changes to the files part1.c, utils.c, and utils.h will be considered by the autograder. The test environment on autolab is Ubuntu 16.04 with gcc 5.x.
Your second task is to complete the emulator by implementing the execute_instruction(), execute()'s, store(), and load() methods in part2.c
This part will consist of implementing the functionality of each instruction. Please implement the functions outlined below (all in part2.c).
Here is an implementation guideline for you. To save your time spent on understanding the whole framework, please consider the following tips.
We have provided a simple self-checking assembly test that tests several of the instructions. However, the test is not exhaustive and does not exercise every instruction. Here's how to run the test (the output is from a working processor).
$ make part2
gcc -Wall -Werror -Wfatal-errors -O2 -o riscv utils.c part1.c part2.c riscv.c
simple_execute TEST PASSED!
multiply_execute TEST PASSED!
random_execute TEST PASSED!
-----------Execute Tests Complete-----------
Most likely you will have bugs, so try the tracing mode or other debugging modes described in the Framework section above.
We have provided a few more tests and the ability to write your own. Just like part1, you will have to create .input files and put them in the relevant folders. However, for part 2, you will want to name your solution file with a .trace instead.
Now build your assembly test, and then run it by typing in the following commands:
$ make [test_name]_execute
You can, and indeed should, write your own assembly tests to test specific instructions and their corner cases. Furthermore, you should be compiling and testing your code after each group of instructions you implement. It will be very hard to debug your project if you wait until the end to test.
For the final results, only changes to the files part1.c, part2.c, utils.c, and utils.h will be considered by the autograder. The test environment on autolab is again Ubuntu 16.04 with gcc 5.x.