-
Notifications
You must be signed in to change notification settings - Fork 140
Functional Simulator
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! |
---|
We are going to reuse our functional memory model.
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; }
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;
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();
};
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);
};
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
}
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 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());
// ...
}
Simple call of execute
method:
instr.execute();
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 is dumped to the standard output extended by 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 significantly helps us on pipeline simulator development.
MIPT-V / MIPT-MIPS — Cycle-accurate pre-silicon simulation.