-
Notifications
You must be signed in to change notification settings - Fork 141
Assignment 3
In this assignment you will implement 1st simulator — single-cycle implementation.
All requirements remain the same as in previous tasks
You should create a new branch task_3
and create four new files:
func_sim/func_sim.h
func_sim/func_sim.cpp
func_sim/main.cpp
func_sim/Makefile
where class MIPS
will be implemented. Notice that you should create your own Makefile: you may modify Makefiles from previous tasks or create it from scratch. In both cases, you are recommended to read our Makefile guide
Your simulator must support every instruction in our list.
Single-cycle is the simplest architecture implementation. It is based on three basic states:
- All operations are executed strongly sequentially
- Execution of an instruction is not started until the previous one is completely executed (no overlapping)
- All instructions take the same amount of time – a single cycle
These 3 postulates make development of functional simulator very easy. Simulator will have structure with internal state, standalone instructions and one method that will execute instructions.
The internal state of single-cycle implementation will be very simple. We are not going to emulate latches, combination circuits etc. — the only things to emulate are explicit data storages:
- register file
- memory
- program counter
class MIPS {
// storages of internal state
RF* rf;
uint32 PC;
FuncMemory* mem;
};
Register file must be simulated as a class shell around array/vector of registers:
enum RegNum {
/// ....
MAX_REG
};
class RF {
uint32 array[MAX_REG];
public:
// ...
uint32 read( RegNum index) const;
void write( RegNum index, uint32 data);
void reset( RegNum index); // clears register to 0 value
// ....
};
Note: MIPS $zero register can not be overwritten! |
---|
You've implemented functional memory model during A1. We are going to reuse it.
Program counter is a stand-alone register that can be stored in MIPS class. Two methods have to work with it:
uint32 fetch() const { return mem->read( PC); }
void updatePC( const FuncInstr& instr) { PC = instr->new_PC; }
In A2, you've implemented class FuncInstr
. You have to extend it with fields of register values:
class FuncInstr {
// ...
uint32 v_src1;
uint32 v_src2;
uint32 v_dst;
uint32 mem_addr;
uint32 new_PC;
// ...
};
It may look useless for single-cycle implementation, but we'll need it for pipelines in future;
Each operation must be presented as void-void micromethod inside FuncInstr
class. Method execute
selects required method either by switch/case
or function pointer (more preferable).
class FuncInstr {
// ...
void add() { v_dst = v_src1 + v_src2; }
void sub() { v_dst = v_src1 - v_src2; }
void mul();
// ...
void execute();
};
Hint: You may add pointer to function and/or other fields to isaTable |
---|
Branches and jumps have to update program counter, PC. Often these instructions require current PC as a base to a new one. So, we have to pass PC to FuncInstr
constructor and store it inside:
class FuncInstr {
const uint32 PC;
uint32 new_PC;
FuncInstr( uint32 bytes, uint32 PC = 0);
};
FuncInstr( uint32 bytes, uint32 PC) : instr( bytes), PC(PC) {
// ...
Class MIPS
has two public methods:
class MIPS {
public:
MIPS();
void run( const string&, uint instr_to_run);
};
Entry point must initialize MIPS and start values;
int main( int argc, char** argv) {
MIPS* mips = new MIPS;
mips->run(/* argv[1] */, /* argv[2] */);
}
Let's look at run(..)
. This method must load a trace from disk and store it into the memory, and initiate main loop:
void MIPS::run( const string& tr, uint instr_to_run);
// load trace
this->PC = startPC;
for (uint i = 0; i < instr_to_run; ++i) {
uint32 instr_bytes;
// Fetch
// Decode and read sources
// Execute
// Memory access
// Writeback
// Update PC
// Dump
}
As we mentioned before, fetch is read from memory by address stored in PC
. Data is stored in instr_bytes
variable.
instr_bytes = fetch();
Decode stage is performed by disassembler you've completed in A2.
FuncInstr instr( instr_bytes, PC);
Sources read must be implemented in separate class MIPS
method
class FuncInstr {
int get_src1_num_index() const;
int get_src2_num_index() const;
};
void MIPS::read_src( FuncInstr& instr) {
// ...
instr.v_src1 = rf->read( instr.get_src1_num_index());
instr.v_src2 = rf->read( instr.get_src2_num_index());
// ...
}
Simple call of execute
method:
instr.execute();
Again, you have to create standalone methods for loads and stores:
void MIPS::load( FuncInstr& instr) {
instr.v_dst = mem->read( instr.mem_addr);
}
void MIPS::store( const FuncInstr& instr) {
mem->write( instr.mem_addr, instr.v_dst);
}
void MIPS::ld_st( FuncInstr& instr) {
// calls load for loads, store for stores, nothing otherwise
}
Method wb( const FuncInstr& instr)
should be very similar to read_src
.
Again, only 1 line required:
void MIPS::updatePC( const FuncInstr& instr) { PC = instr.new_PC; }
Execution trace must be dumped to the standard output. You may use operator<<
that you've created in A2, but you have to extend its output with values:
std::cout << instr << std::endl;
// add $t1 [0x0000000F], $t2 [0x0000001A], $t3 [0x00000029]
As you can see, almost every stage of simulator is only 1-2 lines long. This encapsulation will significantly help us on pipeline simulator development, so please try to keep it in your code. Good luck!
Your simulator should be built by make funcsim
. Test programs and instructions are available in your branch, <branch-root>/tests/samples
directory. We're going to extend our trace coverage before deadline and going to keep you in touch.
MIPT-V / MIPT-MIPS — Cycle-accurate pre-silicon simulation.