Skip to content

Functional Simulator

Pavel Kryukov edited this page Apr 13, 2017 · 1 revision

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.

Clone this wiki locally