Decoder integrato RiscV

Gli obiettivi di questo tutorial sono:

  • Scopri come l'ISA generato e i decoder binari si integrano tra loro.
  • Scrivere il codice C++ necessario per creare un decoder di istruzioni completo per RiscV RV32I che combina ISA e decoder binari.

Comprendere il decoder di istruzioni

Il decoder di istruzioni è responsabile, dato un indirizzo di istruzione, la parola di istruzione dalla memoria e restituire un'istanza Instruction che rappresenta l'istruzione.

Il decoder di primo livello implementa il generic::DecoderInterface mostrato di seguito:

// This is the simulator's interface to the instruction decoder.
class DecoderInterface {
 public:
  // Return a decoded instruction for the given address. If there are errors
  // in the instruciton decoding, the decoder should still produce an
  // instruction that can be executed, but its semantic action function should
  // set an error condition in the simulation when executed.
  virtual Instruction *DecodeInstruction(uint64_t address) = 0;
  virtual ~DecoderInterface() = default;
};

Come puoi vedere, esiste un solo metodo da implementare: cpp virtual Instruction *DecodeInstruction(uint64_t address);

Ora vediamo cosa viene fornito e cosa serve dal codice generato.

Considera innanzitutto la classe di primo livello RiscV32IInstructionSet nel file riscv32i_decoder.h, generato alla fine del tutorial nella decodificatore ISA. Per visualizzare nuovamente i contenuti, vai alla directory della soluzione di quel tutorial e ricreare il tutto.

$ cd riscv_isa_decoder/solution
$ bazel build :all
...<snip>...

Ora reimposta la directory alla radice del repository, quindi diamo un'occhiata alle origini generate. Per farlo, cambia la directory in bazel-out/k8-fastbuild/bin/riscv_isa_decoder (supponendo che tu stia utilizzando un dispositivo x86) host - per altri host, k8-fastbuild sarà un'altra stringa).

$ cd ../..
$ cd bazel-out/k8-fastbuild/bin/riscv_isa_decoder

Vengono visualizzati i quattro file sorgente che contengono il codice C++ generato:

  • riscv32i_decoder.h
  • riscv32i_decoder.cc
  • riscv32i_enums.h
  • riscv32i_enums.cc

Apri il primo file riscv32i_decoder.h. Esistono tre classi dare un'occhiata a:

  • RiscV32IEncodingBase
  • RiscV32IInstructionSetFactory
  • RiscV32IInstructionSet

Prendi nota della denominazione delle classi. Il nome di tutti i corsi si basa sul Versione pascal del nome nella "isa" dichiarazione in tale file: isa RiscV32I { ... }

Iniziamo prima dal corso RiscVIInstructionSet. Viene mostrato di seguito:

class RiscV32IInstructionSet {
 public:
  RiscV32IInstructionSet(ArchState *arch_state,
                         RiscV32IInstructionSetFactory *factory);
  Instruction *Decode(uint64 address, RiscV32IEncodingBase *encoding);

 private:
  std::unique_ptr<Riscv32Slot> riscv32_decoder_;
  ArchState *arch_state_;
};

Non esistono metodi virtuali in questa classe, quindi è una classe autonoma, ma due cose. Innanzitutto, il costruttore prende un puntatore a un'istanza RiscV32IInstructionSetFactory corso. Si tratta di una classe che ha generato decoder usa per creare un'istanza della classe RiscV32Slot, che viene utilizzata decodificare tutte le istruzioni definite per slot RiscV32 come definito in riscv32i.isa file. In secondo luogo, il metodo Decode richiede un parametro aggiuntivo di tipo puntatore a RiscV32IEncodingBase, questa è una classe che fornirà tra il decoder isa generato nel primo tutorial e il file binario decoder generato nel secondo lab.

La classe RiscV32IInstructionSetFactory è una classe astratta da cui ricavare la nostra implementazione per il decoder completo. Nella maggior parte dei casi, è banale: indica semplicemente un metodo per chiamare il costruttore per ogni slot definita nel nostro file .isa. Nel nostro caso, è molto semplice perché è solo una singola classe di questo tipo: Riscv32Slot (il caso pascal del nome riscv32 concatenata con Slot). Il metodo non viene generato automaticamente perché alcuni casi d'uso avanzati in cui potrebbe esserci utilità nel ricavare una sottoclasse dallo slot, chiamando il relativo costruttore.

Esamineremo l'ultimo corso RiscV32IEncodingBase più avanti tutorial, in quanto è l'oggetto di un altro esercizio.


Definisci decoder di istruzioni di primo livello

Definisci la classe di fabbrica

Se hai ricreato il progetto per il primo tutorial, assicurati di tornare a nella directory riscv_full_decoder.

Apri il file riscv32_decoder.h. Tutti i file di inclusione necessari contengono è già stato aggiunto e gli spazi dei nomi sono stati configurati.

Dopo il commento contrassegnato come //Exercise 1 - step 1, definisci il corso RiscV32IsaFactory che eredita da RiscV32IInstructionSetFactory.

class RiscV32IsaFactory : public RiscV32InstructionSetFactory {};

Definisci quindi l'override per CreateRiscv32Slot. Poiché non utilizziamo nessuna classi derivate di Riscv32Slot, è sufficiente allocare una nuova istanza utilizzando std::make_unique.

std::unique_ptr<Riscv32Slot> CreateRiscv32Slot(ArchState *) override {
  return std::make_unique<Riscv32Slot>(state);
}

Se hai bisogno di aiuto (o vuoi controllare il tuo lavoro), la risposta completa è qui

Definisci la classe decoder

Dichiarazioni di costruttori, distruttori e metodi

A questo punto occorre definire la classe decoder. Nello stesso file di cui sopra, vai alla sezione dichiarazione di RiscV32Decoder. Espandi la dichiarazione in una definizione di classe dove RiscV32Decoder eredita da generic::DecoderInterface.

class RiscV32Decoder : public generic::DecoderInterface {
  public:
};

Prima di scrivere il costruttore, diamo una rapida occhiata al codice nel nostro secondo tutorial sul decoder binario. Oltre a tutte le Extract funzioni, ecco la funzione DecodeRiscVInst32:

OpcodeEnum DecodeRiscVInst32(uint32_t inst_word);

Questa funzione prende la parola di istruzione che deve essere decodificata e restituisce il codice operativo che corrisponde all'istruzione. D'altra parte, DecodeInterface classe che RiscV32Decoder implementa solo i passaggi in una . Pertanto, la classe RiscV32Decoder deve poter accedere alla memoria leggi la parola di istruzione da passare a DecodeRiscVInst32(). In questo progetto accedere alla memoria tramite una semplice interfaccia di memoria definita .../mpact/sim/util/memory denominato util::MemoryInterface, di seguito:

  // Load data from address into the DataBuffer, then schedule the Instruction
  // inst (if not nullptr) to be executed (using the function delay line) with
  // context. The size of the data access is based on size of the data buffer.
  virtual void Load(uint64_t address, DataBuffer *db, Instruction *inst,
                    ReferenceCount *context) = 0;

Inoltre, dobbiamo poter passare un'istanza della classe state alla costruttori delle altre classi decoder. La classe di stato appropriata è Classe riscv::RiscVState, che deriva da generic::ArchState, con aggiunta per RiscV. Ciò significa che dobbiamo dichiarare il costruttore può portare un puntatore al state e al memory:

RiscV32Decoder(riscv::RiscVState *state, util::MemoryInterface *memory);

Elimina il costruttore predefinito e sostituisci il distruttore:

RiscV32Decoder() = delete;
~RiscV32Decoder() override;

Poi dichiara il metodo DecodeInstruction da cui eseguire l'override generic::DecoderInterface.

generic::Instruction *DecodeInstruction(uint64_t address) override;

Se hai bisogno di aiuto (o vuoi controllare il tuo lavoro), la risposta completa è qui


Definizioni dei membri dei dati

La classe RiscV32Decoder avrà bisogno di membri di dati privati per archiviare dei parametri del costruttore e un puntatore alla classe fabbrica.

 private:
  riscv::RiscVState *state_;
  util::MemoryInterface *memory_;

Richiede anche un puntatore alla classe di codifica derivata RiscV32IEncodingBase, chiamiamolo RiscV32IEncoding (implementeremo questo nell'esercizio 2). Inoltre ha bisogno di un puntatore a un'istanza RiscV32IInstructionSet, quindi aggiungi:

  RiscV32IsaFactory *riscv_isa_factory_;
  RiscV32IEncoding *riscv_encoding_;
  RiscV32IInstructionSet *riscv_isa_;

Infine, dobbiamo definire un membro di dati da utilizzare con la nostra interfaccia di memoria:

  generic::DataBuffer *inst_db_;

Se hai bisogno di aiuto (o vuoi controllare il tuo lavoro), la risposta completa è qui

Definire i metodi delle classi di decodifica

A questo punto occorre implementare il costruttore, il distruttore DecodeInstruction. Apri il file riscv32_decoder.cc. Il vuoto sono già presenti nel file, così come dichiarazioni dello spazio dei nomi e un paio di using dichiarazioni.

Definizione di costruttore

Il costruttore deve inizializzare solo i membri dei dati. Inizializziamo state_ e memory_:

RiscV32Decoder::RiscV32Decoder(riscv::RiscVState *state,
                               util::MemoryInterface *memory)
    : state_(state), memory_(memory) {

Successivamente, alloca le istanze di ciascuna delle classi relative al decoder, passando i parametri appropriati.

  // Allocate the isa factory class, the top level isa decoder instance, and
  // the encoding parser.
  riscv_isa_factory_ = new RiscV32IsaFactory();
  riscv_isa_ = new RiscV32IInstructionSet(state, riscv_isa_factory_);
  riscv_encoding_ = new RiscV32IEncoding(state);

Infine, alloca l'istanza DataBuffer. Viene allocato utilizzando un modello accessibile tramite il membro state_. Assegniamo le dimensioni di un buffer di dati un singolo uint32_t, che è la dimensione della parola di istruzione.

  inst_db_ = state_->db_factory()->Allocate<uint32_t>(1);

Definizione di distruttore

Il distruttore è semplice, basta liberare gli oggetti allocati nel costruttore, ma con un solo tocco. L'istanza del buffer di dati viene conteggiata come riferimento, quindi invece richiamando delete su quel puntatore, DecRef() l'oggetto:

RiscV32Decoder::~RiscV32Decoder() {
  inst_db_->DecRef();
  delete riscv_isa_;
  delete riscv_isa_factory_;
  delete riscv_encoding_;
}

Definizione del metodo

Nel nostro caso, l'implementazione di questo metodo è piuttosto semplice. Supporremo che l'indirizzo sia allineato correttamente e che non ci siano ulteriori controlli degli errori. obbligatorio.

Innanzitutto, la parola di istruzione deve essere recuperata dalla memoria utilizzando e l'istanza DataBuffer.

  memory_->Load(address, inst_db_, nullptr, nullptr);
  uint32_t iword = inst_db_->Get<uint32_t>(0);

Quindi, chiamiamo l'istanza RiscVIEncoding per analizzare la parola di istruzione, che deve essere fatto prima di chiamare il decoder ISA stesso. Ricorda che l'ISA decoder gestisce direttamente l'istanza RiscVIEncoding per ottenere l'opcode e operandi specificati dalla parola di istruzione. Non l'abbiamo implementato ma usiamo void ParseInstruction(uint32_t) come metodo.

  riscv_encoding_->ParseInstruction(iword);

Infine, chiamiamo il decoder ISA, passando l'indirizzo e la classe Encoding.

  auto *instruction = riscv_isa_->Decode(address, riscv_encoding_);
  return instruction;

Se hai bisogno di aiuto (o vuoi controllare il tuo lavoro), la risposta completa è qui


La classe di codifica

La classe di codifica implementa un'interfaccia utilizzata dalla classe decoder per ottenere l'opcode dell'istruzione, gli operandi di origine e di destinazione degli operandi delle risorse. Questi oggetti dipendono tutti da informazioni decoder, come il codice operativo, i valori di campi specifici parola di istruzione ecc. È separato dalla classe decoder per mantenerla indipendente dalla codifica e abilitare il supporto di più schemi di codifica diversi in futuro.

RiscV32IEncodingBase è una classe astratta. L'insieme di metodi a nostra disposizione da implementare nella nostra classe derivata.

class RiscV32IEncodingBase {
 public:
  virtual ~RiscV32IEncodingBase() = default;

  virtual OpcodeEnum GetOpcode(SlotEnum slot, int entry) = 0;

  virtual ResourceOperandInterface *
              GetSimpleResourceOperand(SlotEnum slot, int entry, OpcodeEnum opcode,
                                       SimpleResourceVector &resource_vec, int end) = 0;

  virtual ResourceOperandInterface *
              GetComplexResourceOperand(SlotEnum slot, int entry, OpcodeEnum opcode,
                                        ComplexResourceEnum resource_op,
                                        int begin, int end) = 0;

  virtual PredicateOperandInterface *
              GetPredicate(SlotEnum slot, int entry, OpcodeEnum opcode,
                           PredOpEnum pred_op) = 0;

  virtual SourceOperandInterface *
              GetSource(SlotEnum slot, int entry, OpcodeEnum opcode,
                        SourceOpEnum source_op, int source_no) = 0;

  virtual DestinationOperandInterface *
              GetDestination(SlotEnum slot, int entry, OpcodeEnum opcode,
                             DestOpEnum dest_op, int dest_no, int latency) = 0;

  virtual int GetLatency(SlotEnum slot, int entry, OpcodeEnum opcode,
                         DestOpEnum dest_op, int dest_no) = 0;
};

A prima vista sembra un po' complicato, in particolare per il numero di ma per un'architettura semplice come RiscV in realtà ignoriamo la maggior parte dei i parametri, in quanto i relativi valori saranno impliciti.

Vediamo i metodi uno alla volta.

OpcodeEnum GetOpcode(SlotEnum slot, int entry);

Il metodo GetOpcode restituisce il membro OpcodeEnum per l'attuale istruzione, identificando il codice operativo dell'istruzione. Il corso OpcodeEnum è definita nel file decoder ISA generato riscv32i_enums.h. Il metodo richiede due parametri, entrambi ignorabili ai nostri scopi. Il primo di questo è il tipo di slot (una classe enum definita anche in riscv32i_enums.h), che, poiché RiscV ha un solo slot, ha un solo valore possibile: SlotEnum::kRiscv32. Il secondo è il numero di istanza dello slot (nel caso sono presenti più istanze dello slot, che possono verificarsi in alcune architetture di progetto).

ResourceOperandInterface *
    GetSimpleResourceOperand(SlotEnum slot, int entry, OpcodeEnum opcode,
                                     SimpleResourceVector &resource_vec, int end)

ResourceOperandInterface *
    GetComplexResourceOperand(SlotEnum slot, int entry, OpcodeEnum opcode,
                                      ComplexResourceEnum resource_op,
                                      int begin, int end);

I due metodi successivi vengono usati per modellare le risorse hardware nel processore per migliorare la precisione del ciclo. Per i nostri esercizi tutorial, non utilizzeremo in modo che nell'implementazione vengano testati, restituendo nullptr.

PredicateOperandInterface *
            GetPredicate(SlotEnum slot, int entry, OpcodeEnum opcode,
                         PredOpEnum pred_op);

SourceOperandInterface *
            GetSource(SlotEnum slot, int entry, OpcodeEnum opcode,
                      SourceOpEnum source_op, int source_no);

DestinationOperandInterface *
            GetDestination(SlotEnum slot, int entry, OpcodeEnum opcode,
                           DestOpEnum dest_op, int dest_no, int latency);

Questi tre metodi restituiscono puntatori agli oggetti operando utilizzati all'interno le funzioni semantiche dell'istruzione per accedere al valore di qualsiasi istruzione operando del predicato, ciascuno degli operandi dell'origine dell'istruzione, e scrive nuovi agli operandi di destinazione dell'istruzione. Poiché RiscV non utilizza predicati di istruzione, quel metodo deve restituire solo nullptr.

Il pattern dei parametri è simile in queste funzioni. Innanzitutto, proprio come GetOpcode lo slot e la voce sono stati passati. Quindi l'opcode per il per la quale deve essere creato l'operando. Viene utilizzato solo se codici operativi diversi devono restituire oggetti operando diversi per lo stesso operando che non avviene in questo simulatore RiscV.

Poi c'è la voce Predicato, Origine e Destinazione, enumerazione operando identifica l'operando da creare. Questi provengono dai tre OpEnums in riscv32i_enums.h come illustrato di seguito:

  enum class PredOpEnum {
    kNone = 0,
    kPastMaxValue = 1,
  };

  enum class SourceOpEnum {
    kNone = 0,
    kBimm12 = 1,
    kCsr = 2,
    kImm12 = 3,
    kJimm20 = 4,
    kRs1 = 5,
    kRs2 = 6,
    kSimm12 = 7,
    kUimm20 = 8,
    kUimm5 = 9,
    kPastMaxValue = 10,
  };

  enum class DestOpEnum {
    kNone = 0,
    kCsr = 1,
    kNextPc = 2,
    kRd = 3,
    kPastMaxValue = 4,
  };

Se torni a guardare riscv32.isa noti che corrispondono agli insiemi di origini e destinazioni nomi degli operandi usati nella dichiarazione di ogni istruzione. Utilizzando diverse nomi degli operandi per gli operandi che rappresentano diversi campi di bit e operando semplifica la scrittura della classe di codifica in quanto membro enum univoco determina l'esatto tipo di operando da restituire e non è necessario considera i valori dei parametri di slot, voce o opcode.

Infine, per gli operandi di origine e di destinazione, la posizione ordinale dei l'operando viene passato (anche in questo caso possiamo ignorarlo) e per la destinazione operando, la latenza (in cicli) che intercorre tra il momento in cui l'istruzione e il risultato della destinazione è disponibile per le istruzioni successive. Nel nostro simulatore la latenza sarà pari a 0, il che significa che l'istruzione scrive il risultato immediatamente al registro.

int GetLatency(SlotEnum slot, int entry, OpcodeEnum opcode,
                         DestOpEnum dest_op, int dest_no);

La funzione finale viene utilizzata per ottenere la latenza di una destinazione specifica operando se è stato specificato come * nel file .isa. È raro, e non viene utilizzata per questo simulatore RiscV, quindi la nostra implementazione di questa funzione restituirà solo 0.


Definisci la classe di codifica

File di intestazione (.h)

Metodi

Apri il file riscv32i_encoding.h. Tutti i file di inclusione necessari contengono è già stato aggiunto e gli spazi dei nomi sono stati configurati. L'aggiunta di tutti i codici è ha finito di seguire il commento // Exercise 2.

Per iniziare, definiamo una classe RiscV32IEncoding che eredita l'interfaccia generata.

class RiscV32IEncoding : public RiscV32IEncodingBase {
 public:

};

Il costruttore deve quindi usare un puntatore all'istanza di stato, un puntatore a riscv::RiscVState. Deve essere usato il distruttore predefinito.

explicit RiscV32IEncoding(riscv::RiscVState *state);
~RiscV32IEncoding() override = default;

Prima di aggiungere tutti i metodi dell'interfaccia, aggiungiamo il metodo chiamato RiscV32Decoder per analizzare l'istruzione:

void ParseInstruction(uint32_t inst_word);

Ora aggiungiamo i metodi che hanno override banali pur eliminando dei parametri che non vengono utilizzati:

// Trivial overrides.
ResourceOperandInterface *GetSimpleResourceOperand(SlotEnum, int, OpcodeEnum,
                                                   SimpleResourceVector &,
                                                   int) override {
  return nullptr;
}

ResourceOperandInterface *GetComplexResourceOperand(SlotEnum, int, OpcodeEnum,
                                                    ComplexResourceEnum ,
                                                    int, int) override {
  return nullptr;
}

PredicateOperandInterface *GetPredicate(SlotEnum, int, OpcodeEnum,
                                        PredOpEnum) override {
  return nullptr;
}

int GetLatency(SlotEnum, int, OpcodeEnum, DestOpEnum, int) override { return 0; }

Infine, aggiungi gli altri override del metodo dell'interfaccia pubblica, ma con le implementazioni differite al file .cc.


OpcodeEnum GetOpcode(SlotEnum, int) override;

SourceOperandInterface *GetSource(SlotEnum , int, OpcodeEnum,
                                  SourceOpEnum source_op, int) override;

DestinationOperandInterface *GetDestination(SlotEnum, int, OpcodeEnum,
                                            DestOpEnum dest_op, int,
                                            int latency) override;

Al fine di semplificare l'implementazione di ognuno dei metodi getter degli operandi creeremo due array di callables (oggetti funzione) indicizzati dal valore numerico rispettivamente dei membri SourceOpEnum e DestOpEnum. In questo modo il corpo di questi elementi nei metodi è ridotto a chiamare il metodo per il valore enum che viene passato e che restituisce come risultato valore.

Per organizzare l'inizializzazione di questi due array, definiamo due che verranno chiamati dal costruttore nel seguente modo:

 private:
  void InitializeSourceOperandGetters();
  void InitializeDestinationOperandGetters();

Membri dei dati

I membri dei dati richiesti sono i seguenti:

  • state_ per contenere il valore riscv::RiscVState *.
  • inst_word_ di tipo uint32_t che contiene il valore del valore attuale parola di istruzione.
  • opcode_ per conservare il codice operativo dell'istruzione corrente che viene aggiornato da il metodo ParseInstruction. Questo ha il tipo OpcodeEnum.
  • source_op_getters_ un array per archiviare i chiamabili utilizzati per ottenere l'origine oggetti operando. Il tipo di elementi di un array è absl::AnyInvocable<SourceOperandInterface *>()>.
  • dest_op_getters_ un array per archiviare i chiamabili utilizzati per ottenere oggetti operando di destinazione. Il tipo di elementi di un array è absl::AnyInvocable<DestinationOperandInterface *>()>.
  • xreg_alias un array di nomi ABI del registro intero RiscV, ad esempio "zero" e "ra" anziché "x0" e "x1".

  riscv::RiscVState *state_;
  uint32_t inst_word_;
  OpcodeEnum opcode_;

  absl::AnyInvocable<SourceOperandInterface *()>
      source_op_getters_[static_cast<int>(SourceOpEnum::kPastMaxValue)];
  absl::AnyInvocable<DestinationOperandInterface *(int)>
      dest_op_getters_[static_cast<int>(DestOpEnum::kPastMaxValue)];

  const std::string xreg_alias_[32] = {
      "zero", "ra", "sp", "gp", "tp",  "t0",  "t1", "t2", "s0", "s1", "a0",
      "a1",   "a2", "a3", "a4", "a5",  "a6",  "a7", "s2", "s3", "s4", "s5",
      "s6",   "s7", "s8", "s9", "s10", "s11", "t3", "t4", "t5", "t6"};

Se hai bisogno di aiuto (o vuoi controllare il tuo lavoro), la risposta completa è qui

File di origine (.cc).

Apri il file riscv32i_encoding.cc. Tutti i file di inclusione necessari contengono è già stato aggiunto e gli spazi dei nomi sono stati configurati. L'aggiunta di tutti i codici è ha finito di seguire il commento // Exercise 2.

Funzioni helper

Inizieremo scrivendo un paio di funzioni helper che utilizziamo per creare degli operandi dei registri di origine e di destinazione. Questi modelli verranno presi in considerazione registry type e chiama l'oggetto RiscVState per ottenere un handle registro oggetto, quindi chiamare un metodo di fabbrica operando nell'oggetto registri.

Iniziamo con gli helper dell'operando di destinazione:

template <typename RegType>
inline DestinationOperandInterface *GetRegisterDestinationOp(
    RiscVState *state, const std::string &name, int latency) {
  auto *reg = state->GetRegister<RegType>(name).first;
  return reg->CreateDestinationOperand(latency);
}

template <typename RegType>
inline DestinationOperandInterface *GetRegisterDestinationOp(
    RiscVState *state, const std::string &name, int latency,
    const std::string &op_name) {
  auto *reg = state->GetRegister<RegType>(name).first;
  return reg->CreateDestinationOperand(latency, op_name);
}

Come puoi vedere, ci sono due funzioni helper. Il secondo richiede un'ulteriore parametro op_name che consente all'operando di avere un nome o una stringa diversi rappresentativa del registro di base.

In modo analogo per gli helper degli operandi di origine:

template <typename RegType>
inline SourceOperandInterface *GetRegisterSourceOp(RiscVState *state,
                                                   const std::string &reg_name) {
  auto *reg = state->GetRegister<RegType>(reg_name).first;
  auto *op = reg->CreateSourceOperand();
  return op;
}

template <typename RegType>
inline SourceOperandInterface *GetRegisterSourceOp(RiscVState *state,
                                                   const std::string &reg_name,
                                                   const std::string &op_name) {
  auto *reg = state->GetRegister<RegType>(reg_name).first;
  auto *op = reg->CreateSourceOperand(op_name);
  return op;
}

Funzioni costruttore e interfaccia

Il costruttore e le funzioni di interfaccia sono molto semplici. Il costruttore chiama semplicemente i due metodi di inizializzazione per inizializzare gli array callables per i getter dell'operando.

RiscV32IEncoding::RiscV32IEncoding(RiscVState *state) : state_(state) {
  InitializeSourceOperandGetters();
  InitializeDestinationOperandGetters();
}

ParseInstruction memorizza la parola di istruzione e poi il codice operativo ottiene richiamando il codice generato dal decoder binario.

// Parse the instruction word to determine the opcode.
void RiscV32IEncoding::ParseInstruction(uint32_t inst_word) {
  inst_word_ = inst_word;
  opcode_ = mpact::sim::codelab::DecodeRiscVInst32(inst_word_);
}

Infine, i getter dell'operando restituiscono il valore dalla funzione getter che chiama in base alla ricerca nell'array utilizzando il valore enum dell'operando destinazione/origine.


DestinationOperandInterface *RiscV32IEncoding::GetDestination(
    SlotEnum, int, OpcodeEnum, DestOpEnum dest_op, int, int latency) {
  return dest_op_getters_[static_cast<int>(dest_op)](latency);
}

SourceOperandInterface *RiscV32IEncoding::GetSource(SlotEnum, int, OpcodeEnum,
                                                    SourceOpEnum source_op, int) {
  return source_op_getters_[static_cast<int>(source_op)]();
}

Metodi di inizializzazione degli array

Come avrai intuito, la maggior parte del lavoro consiste nell'inizializzare il comando getter ma non preoccuparti, si può fare utilizzando una semplice sequenza ricorrente. Iniziamo iniziano prima con InitializeDestinationOpGetters(), poiché c'è solo coppia di operandi di destinazione.

Ricorda il corso DestOpEnum generato da riscv32i_enums.h:

  enum class DestOpEnum {
    kNone = 0,
    kCsr = 1,
    kNextPc = 2,
    kRd = 3,
    kPastMaxValue = 4,
  };

Per dest_op_getters_ dobbiamo inizializzare 4 voci, una per kNone, kCsr, kNextPc e kRd. Per praticità, ogni voce viene inizializzata con lambda, sebbene sia possibile usare anche qualsiasi altra forma di callable. La firma di lambda è void(int latency).

Finora non abbiamo parlato molto dei diversi tipi di destinazioni operandi definiti in MPACT-Sim. Per questo esercizio utilizzeremo solo due tipi: generic::RegisterDestinationOperand definiti in register.h, e generic::DevNullOperand definiti in devnull_operand.h. I dettagli di questi operandi non sono molto importanti al momento, ad eccezione del fatto che la prima è utilizzata per scrivere nei registri, mentre la seconda ignora tutte le scritture.

La prima voce per kNone è banale: è sufficiente restituire un nullptr e, facoltativamente, il valore registrare un errore.

void RiscV32IEncoding::InitializeDestinationOperandGetters() {
  // Destination operand getters.
  dest_op_getters_[static_cast<int>(DestOpEnum::kNone)] = [](int) {
    return nullptr;
  };

Poi c'è kCsr. Qui andremo a imbrogliare un po'. Il "Hello World" programma non si basa su alcun aggiornamento effettivo del CSR, ma c'è un codice boilerplate che eseguire le istruzioni per la richiesta CSR. La soluzione è fare un esempio utilizzando un registro regolare denominato "CSR" e incanalare tutte queste scritture.

  dest_op_getters_[static_cast<int>(DestOpEnum::kCsr)] = [this](int latency) {
    return GetRegisterDestinationOp<RV32Register>(state_, "CSR", latency);
  };

Poi c'è kNextPc, che fa riferimento al "pc" registro. È utilizzato come target per tutte le istruzioni di ramo e salto. Il nome è definito in RiscVState come kPcName.

  dest_op_getters_[static_cast<int>(DestOpEnum::kNextPc)] = [this](int latency) {
    return GetRegisterDestinationOp<RV32Register>(state_, RiscVState::kPcName, latency);
  }

Infine c'è l'operando di destinazione kRd. Nell'operando riscv32i.isa rd viene utilizzato solo per fare riferimento al registro intero codificato nel carattere "rd" campo della parola di istruzione, per cui non vi sia ambiguità. Là è solo una delle complicazioni. Il registro x0 (nome abi zero) è cablato su 0, quindi per il registro utilizziamo DevNullOperand.

Quindi in questo getter estraiamo per prima cosa il valore nel campo rd utilizzando Metodo Extract generato dal file .bin_fmt. Se il valore è 0, restituisce un valore "DevNull" operando, altrimenti viene restituito l'operando di registro corretto. avendo cura di usare l'alias del registro appropriato come nome dell'operando.

  dest_op_getters_[static_cast<int>(DestOpEnum::kRd)] = [this](int latency) {
    // First extract register number from rd field.
    int num = inst32_format::ExtractRd(inst_word_);
    // For register x0, return the DevNull operand.
    if (num == 0) return new DevNullOperand<uint32_t>(state, {1});
    // Return the proper register operand.
    return GetRegisterDestinationOp<RV32Register>(
      state_, absl::StrCat(RiscVState::kXRegPrefix, num), latency,
      xreg_alias_[num]);
    )
  }
}

Passiamo ora al metodo InitializeSourceOperandGetters(), dove il pattern è più o meno lo stesso, ma i dettagli differiscono leggermente.

Innanzitutto, diamo un'occhiata al SourceOpEnum generato riscv32i.isa nel primo tutorial:

  enum class SourceOpEnum {
    kNone = 0,
    kBimm12 = 1,
    kCsr = 2,
    kImm12 = 3,
    kJimm20 = 4,
    kRs1 = 5,
    kRs2 = 6,
    kSimm12 = 7,
    kUimm20 = 8,
    kUimm5 = 9,
    kPastMaxValue = 10,
  };

Dall'esame dei membri, oltre a kNone, rientrano in due gruppi. Uno. sono operandi immediati: kBimm12, kImm12, kJimm20, kSimm12, kUimm20, e kUimm5. Gli altri sono operandi di registri: kCsr, kRs1 e kRs2.

L'operando kNone viene gestito come per gli operandi di destinazione. Restituisce un nullptr.

void RiscV32IEncoding::InitializeSourceOperandGetters() {
  // Source operand getters.
  source_op_getters_[static_cast<int>(SourceOpEnum::kNone)] = [] () {
    return nullptr;
  };

Ora lavoriamo sugli operandi del registro. Gestiremo le kCsr al modo in cui abbiamo gestito gli operandi di destinazione corrispondenti, basta richiamare funzione helper utilizzando "CSR" come nome del registro.

  // Register operands.
  source_op_getters_[static_cast<int>(SourceOpEnum::kCsr)] = [this]() {
    return GetRegisterSourceOp<RV32Register>(state_, "CSR");
  };

Gli operando kRs1 e kRs2 vengono gestiti in modo equivalente a kRd, ad eccezione di anche se non volevamo aggiornare x0 (o zero), vogliamo assicurarci leggiamo sempre 0 dall'operando. A questo scopo utilizzeremo generic::IntLiteralOperand<> corso definito in literal_operand.h Questo operando viene utilizzato per archiviare un valore letterale (anziché un valore valore immediato). In caso contrario, il pattern è lo stesso: estrai prima il Il valore rs1/rs2 della parola di istruzione, se è zero, restituisce il valore letterale operando con un parametro di modello pari a 0, altrimenti restituisce un registro regolare operando di origine utilizzando la funzione helper e utilizzando l'alias abi come operando nome.

  source_op_getters_[static_cast<int>(SourceOpEnum::kRs1)] =
      [this]() -> SourceOperandInterface * {
    int num = inst32_format::ExtractRs1(inst_word_);
    if (num == 0) return new IntLiteralOperand<0>({1}, xreg_alias_[0]);
    return GetRegisterSourceOp<RV32Register>(
        state_, absl::StrCat(RiscVState::kXregPrefix, num), xreg_alias_[num]);
  };
  source_op_getters_[static_cast<int>(SourceOpEnum::kRs2)] =
      [this]() -> SourceOperandInterface * {
    int num = inst32_format::ExtractRs2(inst_word_);
    if (num == 0) return new IntLiteralOperand<0>({1}, xreg_alias_[0]);
    return GetRegisterSourceOp<RV32Register>(
        state_, absl::StrCat(RiscVState::kXregPrefix, num), xreg_alias_[num]);
  };

Infine, gestiamo i diversi operandi immediati. I valori immediati sono archiviati in istanze della classe generic::ImmediateOperand<> definita immediate_operand.h L'unica differenza tra i diversi getter per gli operandi immediati è la funzione estrattore utilizzata e se il tipo di archiviazione è firmato o senza firma, in base al campo di bit.

  // Immediates.
  source_op_getters_[static_cast<int>(SourceOpEnum::kBimm12)] = [this]() {
    return new ImmediateOperand<int32_t>(
        inst32_format::ExtractBImm(inst_word_));
  };
  source_op_getters_[static_cast<int>(SourceOpEnum::kImm12)] = [this]() {
    return new ImmediateOperand<int32_t>(
        inst32_format::ExtractImm12(inst_word_));
  };
  source_op_getters_[static_cast<int>(SourceOpEnum::kUimm5)] = [this]() {
    return new ImmediateOperand<uint32_t>(
        inst32_format::ExtractUimm5(inst_word_));
  };
  source_op_getters_[static_cast<int>(SourceOpEnum::kJimm20)] = [this]() {
    return new ImmediateOperand<int32_t>(
        inst32_format::ExtractJImm(inst_word_));
  };
  source_op_getters_[static_cast<int>(SourceOpEnum::kSimm12)] = [this]() {
    return new ImmediateOperand<int32_t>(
        inst32_format::ExtractSImm(inst_word_));
  };
  source_op_getters_[static_cast<int>(SourceOpEnum::kUimm20)] = [this]() {
    return new ImmediateOperand<uint32_t>(
        inst32_format::ExtractUimm32(inst_word_));
  };
}

Se hai bisogno di aiuto (o vuoi controllare il tuo lavoro), la risposta completa è qui

Il tutorial termina qui. Speriamo che ti sia stato utile.