The student repo for this lab will be based on assignment 2. Before you start the lab make sure you have created and cloned the repo
This lab has been modified by your CMPT 295 instructor and authors of RISC-V.
mac.input
program. Add support for disassembling and running add
,mul
,addi
, sw
and lw
instructions.opcode
and rest
. The other members of the union include structs representing different instruction types.// Lines 48---110 . types.h
typedef union {
/* access opcode with: instruction.opcode */
struct {
unsigned int opcode : 7;
unsigned int rest : 25;
};
/* access rtype with: instruction.rtype.(opcode|rd|funct3|rs1|rs2|funct7) */
struct {
unsigned int opcode : 7;
unsigned int rd : 5;
unsigned int funct3 : 3;
unsigned int rs1 : 5;
unsigned int rs2 : 5;
unsigned int funct7 : 7;
} rtype;
/* access itype with: instruction.itype.(oppcode|rs|rt|imm) */
struct {
unsigned int opcode : 7;
unsigned int rd : 5;
unsigned int funct3 : 3;
unsigned int rs1 : 5;
unsigned int imm : 12;
} itype;
....
}
typedef struct {
Register R[32];
Register PC;
} Processor;
Here is the assembly listing for a program that performs a simple mac operation (multiply and accumulate). RISC V lacks a mac instructions. Hence this has to be accomplished using a sequence of steps. Below we show an annotated assembly code. The assembly code has been compiled to a binary. The binary's layout in memory is shown as well. The instructions start at address 0x1000.
code/ref/mac.solution
lists the reference disassembled program.
# Implement 3*5+10.
addi x10, x10, 3 // Move 0x3 to %x10
addi x11, x11, 5 // Move 0x5 to %x11
addi x12, x12, 10 // Move 0x10 to %x12
mul x13, x10, x11 // Multiple %x10 and %x11 and store in %x13
## Below 2 instruction sequence is not necessary
## We use it to illustrate how to implement memory ops
sw x13, 0(x0) // Save the value onto address 0x0.
lw x11, 0(x0) // Load the value from the 0x0
add x13, x11, x12 # Add value to result. x11 now contains 3*5
addi x11, x13, 0 # Display value on screen
addi x10, x0, 1
ecall
addi x10, x0, 10 # Exit the program
ecall
addi x0, x0, 0
Binary Listing code/input/mac.input
Address | Instruction |
---|---|
0x1000 | 0x00350513 |
0x1004 | 0x00558593 |
0x1008 | 0x00a60613 |
0x100c | 0x02b506b3 |
0x1010 | 0x00d02023 |
0x1014 | 0x00002583 |
0x1018 | 0x00c586b3 |
0x101c | 0x00068593 |
0x1020 | 0x00100513 |
0x1024 | 0x00000073 |
0x1028 | 0x00a00513 |
0x102c | 0x00000073 |
0x1030 | 0x00000013 |
In this module, we are going to read the 32 bits corresponding to each instruction from mac.input, parse the bits, and fill in the fields on Instruction
. We walk through the steps for the R-type instruction and ask you to implement the I and S types. Talk to the TA if you are confused..
Modify parse instruction in utils.c to support R instructions. Flowchart below outlines the steps. The figure also shows the format of the r type instructions. You can obtain the other instruction formats by referring here. RISCV Card
utils.c
instruction_bits & ((1U << 7) - 1)
. This &
s the instruction bits with a mask (bottom 6 1s = 0x0000 0000 0011 11111) to obtain the opcode. Following this shift right out the bottom seven bits to interpret the next set of fields.instruction_bits & ((1U << 5) - 1);
Instruction parse_instruction(uint32_t instruction_bits)
{
/* YOUR CODE HERE */
Instruction instruction;
// add x8, x0, x0 hex : 00000433 binary = 0000 0000 0000 0000 0000 01000 Opcode: 0110011 (0x33)
// 1. Get the Opcode by &ing with 0b 0000 0000 00 111111, bottom 7 bits
instruction.opcode = instruction_bits & ((1U << 7) - 1);
// 2. Shift right to move to pointer to interpret next fields in instruction.
instruction_bits >>= 7;
switch (instruction.opcode)
{
// R-Type
case 0x33:
// instruction: 0000 0000 0000 0000 0000 destination : 01000
instruction.rtype.rd = instruction_bits & ((1U << 5) - 1);
instruction_bits >>= 5;
// instruction: 0000 0000 0000 0000 0 func3 : 000
instruction.rtype.funct3 = instruction_bits & ((1U << 3) - 1);
instruction_bits >>= 3;
// instruction: 0000 0000 0000 src1: 00000
instruction.rtype.rs1 = instruction_bits & ((1U << 5) - 1);
instruction_bits >>= 5;
// instruction: 0000 000 src2: 00000
instruction.rtype.rs2 = instruction_bits & ((1U << 5) - 1);
instruction_bits >>= 5;
// funct7: 0000 000 add
instruction.rtype.funct7 = instruction_bits & ((1U << 7) - 1);
break;
// I-Type
// Do this for I-type and S-type.
RTYPE_FORMAT
in utils.h
instruction.rtype.rd
, instruction.rtype.rs1
, and instruction.rtype.rs2
void print_rtype(char *name, Instruction instruction) {
printf(RTYPE_FORMAT, name, instruction.rtype.rd, instruction.rtype.rs1,
instruction.rtype.rs2);
/* YOUR CODE HERE */
}
Congratulations you are now ready to test the parsing of any R instruction
make riscv
./riscv -d ./code/input/R/R.input
Parsing mac.input requires support for additionally I-type (ecall and addi), S-type (handling the SW).
# Opcodes 0x1110011, 0x0010011, 1110011
instruction.itype.rd = instruction_bits & ((1U << 5) - 1);
instruction_bits >>= 5;
....
instruction.itype.rs1 = instruction_bits & ((1U << 5) - 1);
instruction_bits >>= 5;
instruction.itype.imm = instruction_bits & ((1U << 12) - 1);
int offset = 0x00000000;
offset |= instruction.stype.imm5 & 0x0000001f; // imm[0:4]
offset |= (instruction.stype.imm7 << 5) & 0x00000fe0; // imm[5:11]
int immediate = sign_extend_number(offset, 12);
IF YOU ARE STUCK, YOU CAN ASK THE TA TO HELP YOU. MAKE SURE YOU COMMIT YOUR CODE and SEND A LINK TO THE TA, BEFORE ASKING THE TA.
$ make riscv
$ ./riscv -d ./code/input/mac.input
In this module, we are going to look at the fields of the instruction parse in part 1 and implement the functionality of each instruction on the soft processor (a software data structure representing the hardware). Every RISCV instruction reads or modifies three variables Processor.R[32]
, Processor.R[PC]
, and Memory
. You can think of the emulator as an interpreter that reading the program instruction-by-instruction and invoking the appropriate execute_
method in part2.c
. Each execute_
modifies the processor state.
Description(C)
column on page 1 of the RISC V card RISCV Card.Modify parse instruction in part2.c to support R instructions and I instructions.
Take a look at the output of the disassembler. The R-instruction types present in the mac program are add and mul. We add support in the processor model for these instructions. In part2.c
add
.The semantics of add instruction is rd = rs1 + rs2
. rd and rs1 and rs2 are specified by the instruction. Under the switch case in Line 60: part2.c // Add
in function execute_rtype(Instruction instruction, Processor *processor)
.
you will be implementing the instruction. To implement add do the following.
processor->R[instruction.rtype.rd] =
((sWord)processor->R[instruction.rtype.rs1]) +
((sWord)processor->R[instruction.rtype.rs2]);
What is the above statement doing ? It gets the registers to be modified from instruction bits passed to the function instruction.rtype.rs1,rs2,rd
. It then reads and writes the processor->R
registers. Since the R[32]
is an unsigned variable and + is a signed operator, we cast the Rs to signed (sWord)
before updating the destination register.
Similarly we can implement multiply.
processor->R[instruction.rtype.rd] =
((sWord)processor->R[instruction.rtype.rs1]) *
((sWord)processor->R[instruction.rtype.rs2]);
addi
Addi is similar to add
except the second operand is derived from the instruction as an immediate, not as a register. Further, we need to sign extend it from the 12th bit.
What is sign extension slides 25-28.
processor->R[instruction.itype.rd] =
((sWord)processor->R[instruction.itype.rs1]) +
sign_extend_number(instruction.itype.imm, 12);
# utils.c
int sign_extend_number(unsigned int field, unsigned int n)
{
...
return (int)field << (32 - n) >> (32 - n);
}
sw
The final step before we can run mac is to implement loads and stores. Loads and stores will now deal with processor->Memory
. The execution of the load and store is split into two parts: argument setup and reading/writing from memory.
Let us first implement the stores. # store writes to memory from a register. There are four parameters required by a store, the processors ram, the value to be written, the number of bytes, and the address. The value to be written is obtained by modifying the get_store_offset
in utils.c
.
// utils.c
int get_store_offset(Instruction instruction)
{
/* YOUR CODE HERE */
int offset = 0x00000000;
offset |= instruction.stype.imm5 & 0x0000001f; // imm[0:4]
offset |= (instruction.stype.imm7 << 5) & 0x00000fe0; // imm[5:11]
return sign_extend_number(offset, 12);
return 0;
}
// part2.c execute_store()
case 0x2:
// SW
store(
memory,
((sWord)processor->R[instruction.stype.rs1]) +
((sWord)get_store_offset(instruction)),
LENGTH_WORD,
processor->R[instruction.stype.rs2]);
break;
value & 0x000000ff
obtains byte 0, (value & 0x0000ff00) >> 8 obtains byte 1 and so on.
We store this starting at memory[address]
to memory[address+4]
. In assignment 2 we might be testing with bytes or half words, or words. Make sure you handle those cases.// e.g., value = 0xdeadbeef
// value & 0x000000ff = 0xef
// ((value & 0x0000ff00) >> 8) = 0xbe;
// (Byte)((value & 0x0000ff00) >> 8) = 0xad;
// (Byte)((value & 0x00ff0000) >> 16) = 0xbe
if (alignment == LENGTH_WORD)
{
memory[address] = (Byte)(value & 0x000000ff);
memory[address + 1] = (Byte)((value & 0x0000ff00) >> 8);
memory[address + 2] = (Byte)((value & 0x00ff0000) >> 16);
memory[address + 3] = (Byte)((value & 0xff000000) >> 24);
}
$ make riscv
$ ./riscv -r ./code/input/mac.input
we will let you figure out what the type of memory is