Functional Simulator - MIPT-ILab/mipt-mips GitHub Wiki
Single-cycle implementation
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.
Internal state
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
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! |
---|
Memory
We are going to reuse our functional memory model.
PC
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; }
Instructions
class FuncInstr
is extended 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 need it for pipelines in future;
Execution
Each operation is presented as void-void micromethod inside FuncInstr
class. Method execute
selects required method either by function pointer.
class FuncInstr {
// ...
void add() { v_dst = v_src1 + v_src2; }
void sub() { v_dst = v_src1 - v_src2; }
void mul();
// ...
void execute();
};
PC update
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) {
// ...
Initialization
Class MIPS
has two public methods:
class MIPS {
public:
MIPS();
void run( const string&, uint instr_to_run);
};
Main loop
Let's look at run(..)
. This method loads 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
}
Fetch
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
Decode stage is performed by disassembler you've completed in A2.
FuncInstr instr( instr_bytes, PC);
Read sources
Sources read is 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());
// ...
}
Execution
Simple call of execute
method:
instr.execute();
Memory access
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
}
Writeback
Method wb( const FuncInstr& instr)
should be very similar to read_src
.
Update PC
Again, only 1 line required:
void MIPS::updatePC( const FuncInstr& instr) { PC = instr.new_PC; }
Dump
Execution trace is dumped to the standard output extended by values:
std::cout << instr << std::endl;
// add $t1 [0x0000000F], $t2 [0x0000001A], $t3 [0x00000029]
Summary
As you can see, almost every stage of simulator is only 1-2 lines long. This encapsulation significantly helps us on pipeline simulator development.