Integrierter RiscV-Decoder

Mit dieser Anleitung werden folgende Ziele erreicht:

  • Hier erfahren Sie, wie die generierten ISA- und Binärdecoder zusammenpassen.
  • Erforderlichen C++-Code schreiben, um einen vollständigen Anweisungsdecoder für RiscV zu erstellen RV32I, der ISA und Binärdecoder kombiniert.

Anleitungsdecoder verstehen

Der Anweisungsdecoder hat bei einer Anweisungsadresse die Aufgabe, das Anweisungswort aus dem Speicher und gibt eine vollständig initialisierte Instanz des Instruction, die diese Anweisung darstellt.

Der Decoder der obersten Ebene implementiert den unten gezeigten generic::DecoderInterface:

// 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;
};

Wie Sie sehen, gibt es nur eine Methode, die implementiert werden muss: cpp virtual Instruction *DecodeInstruction(uint64_t address);

Sehen wir uns nun an, was vom generierten Code bereitgestellt wird und was benötigt wird.

Betrachten Sie zuerst die Klasse RiscV32IInstructionSet der obersten Ebene in der Datei riscv32i_decoder.h, der am Ende der Anleitung im ISA-Decoder Wenn Sie sich den Inhalt noch einmal ansehen möchten, wechseln Sie zum Lösungsverzeichnis von und alles neu erstellen.

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

Wechseln Sie nun zurück zum Repository-Stammverzeichnis. Sehen wir uns an den Quellen, die generiert wurden. Ändern Sie dazu das Verzeichnis in bazel-out/k8-fastbuild/bin/riscv_isa_decoder (vorausgesetzt, Sie verwenden ein x86- host – bei anderen Hosts ist der k8-fastbuild ein weiterer String).

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

Die vier Quelldateien, die den generierten C++-Code enthalten, werden aufgelistet:

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

Öffne die erste Datei riscv32i_decoder.h. Es gibt drei Klassen, sollten Sie sich Folgendes ansehen:

  • RiscV32IEncodingBase
  • RiscV32IInstructionSetFactory
  • RiscV32IInstructionSet

Notieren Sie sich die Namen der Klassen. Alle Klassen sind nach dem Version des in „isa“ angegebenen Namens in Groß- und Kleinschreibung Deklaration in dieser Datei: isa RiscV32I { ... }

Fangen wir mit dem Kurs RiscVIInstructionSet an. Hier sehen Sie:

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_;
};

In diesem Kurs gibt es keine virtuellen Methoden, daher ist dies ein eigenständiger Kurs, zwei Dinge beachten. Zuerst nimmt der Konstruktor einen Zeiger auf eine Instanz des Klasse RiscV32IInstructionSetFactory. Dies ist eine Klasse, die vom generierten Decoder verwendet, um eine Instanz der RiscV32Slot-Klasse zu erstellen, die für Folgendes verwendet wird: alle Anweisungen, die für slot RiscV32 definiert sind, wie in der riscv32i.isa-Datei. Zweitens: Für die Methode Decode wird ein zusätzlicher Parameter vom Typ-Zeiger auf RiscV32IEncodingBase handelt, stellt diese Klasse den Schnittstelle zwischen dem in der ersten Anleitung generierten Isa-Decodierer und dem Binärprogramm den wir im zweiten Lab generiert haben.

Die Klasse RiscV32IInstructionSetFactory ist eine abstrakte Klasse, aus der muss eine eigene Implementierung für den vollständigen Decoder ableiten. In den meisten Fällen ist simpel: Stellen Sie einfach eine Methode zum Aufrufen des Konstruktors für jede Slot-Klasse, die in der .isa-Datei definiert ist. In unserem Fall ist es ganz einfach, ist nur eine einzige solche Klasse: Riscv32Slot (Pascal-Großbuchstaben des Namens riscv32) mit Slot verkettet sind. Die Methode wird nicht für Sie generiert, da es einige erweiterte Anwendungsfälle, bei denen es nützlich sein kann, eine Unterklasse abzuleiten aus dem Slot und ruft stattdessen dessen Konstruktor auf.

Den letzten Kurs RiscV32IEncodingBase sehen wir uns später in dieser Lektion an. da dies Gegenstand einer anderen Übung ist.


Anweisungsdecoder der obersten Ebene definieren

Factory-Klasse definieren

Wenn Sie das Projekt für die erste Anleitung neu erstellt haben, stellen Sie sicher, dass Sie wieder zu das Verzeichnis riscv_full_decoder.

Öffnen Sie die Datei riscv32_decoder.h. Alle erforderlichen Einschlussdateien haben wurde bereits hinzugefügt und die Namespaces wurden eingerichtet.

Definieren Sie die Klasse nach dem Kommentar mit der Kennzeichnung //Exercise 1 - step 1. RiscV32IsaFactory übernimmt RiscV32IInstructionSetFactory.

class RiscV32IsaFactory : public RiscV32InstructionSetFactory {};

Definieren Sie als Nächstes die Überschreibung für CreateRiscv32Slot. Da wir keine abgeleiteten Klassen von Riscv32Slot zuweisen, ordnen wir einfach eine neue Instanz std::make_unique.

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

Wenn du Hilfe brauchst oder deine Arbeit überprüfen möchtest, ist die Antwort ganz genau. hier.

Decoderklasse definieren

Konstruktoren, Destruktor- und Methodendeklarationen

Als Nächstes definieren wir die Decoderklasse. Wechseln Sie in derselben Datei wie oben zum Erklärung von RiscV32Decoder. Deklaration zu einer Klassendefinition erweitern Dabei übernimmt RiscV32Decoder die Einstellungen von generic::DecoderInterface.

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

Bevor wir den Konstruktor schreiben, werfen wir einen Blick auf den Code die wir in unserer zweiten Anleitung zum Binärdecoder erstellt haben. Zusätzlich zu all den Extract-Funktionen haben, gibt es die Funktion DecodeRiscVInst32:

OpcodeEnum DecodeRiscVInst32(uint32_t inst_word);

Diese Funktion nimmt das zu decodierende Anweisungswort und gibt den Opcode, der dieser Anweisung entspricht. Im Gegensatz dazu DecodeInterface-Klasse, die von RiscV32Decoder nur Karten/Tickets in einer Adresse. Daher muss die Klasse RiscV32Decoder in der Lage sein, auf den Arbeitsspeicher zuzugreifen, das Anweisungswort lesen, das an DecodeRiscVInst32() übergeben werden soll. In diesem Projekt Der Zugriff auf den Arbeitsspeicher erfolgt über eine einfache Speicherschnittstelle, .../mpact/sim/util/memory mit dem passenden Namen util::MemoryInterface, siehe unten:

  // 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;

Darüber hinaus müssen wir in der Lage sein, eine state-Klasseninstanz an die Konstruktoren der anderen Decoderklassen. Die entsprechende Statusklasse ist Klasse riscv::RiscVState, die aus generic::ArchState abgeleitet ist, mit hinzugefügten für RiscV. Das bedeutet, dass wir den Konstruktor so deklarieren müssen, kann einen Zeiger auf die state und die memory nehmen:

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

Löschen Sie den Standardkonstruktor und überschreiben Sie den Destruktor:

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

Deklarieren Sie als Nächstes die Methode DecodeInstruction, die von generic::DecoderInterface.

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

Wenn du Hilfe brauchst oder deine Arbeit überprüfen möchtest, ist die Antwort ganz genau. hier.


Definitionen von Datenmitgliedern

Die Klasse RiscV32Decoder benötigt private Datenmitglieder, um Folgendes zu speichern: -Konstruktorparameter und einen Zeiger auf die Factory-Klasse.

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

Außerdem ist ein Zeiger auf die Codierungsklasse erforderlich, die von RiscV32IEncodingBase nennen wir das RiscV32IEncoding (wir implementieren dies in Übung 2). Außerdem benötigt es einen Zeiger auf eine Instanz von RiscV32IInstructionSet, also füge Folgendes hinzu:

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

Schließlich müssen wir ein Datenelement zur Verwendung mit unserer Speicherschnittstelle definieren:

  generic::DataBuffer *inst_db_;

Wenn du Hilfe brauchst oder deine Arbeit überprüfen möchtest, ist die Antwort ganz genau. hier.

Methoden der Decoder-Klasse definieren

Als Nächstes implementieren wir den Konstruktor, den Destruktor und den DecodeInstruction-Methode. Öffnen Sie die Datei riscv32_decoder.cc. Die leere sind bereits in der -Datei enthalten, sowie Namespace-Deklarationen und einige von using Deklarationen.

Konstruktor – Definition

Der Konstruktor muss nur die Datenmitglieder initialisieren. Zuerst initialisieren Sie die state_ und memory_:

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

Ordnen Sie als Nächstes Instanzen jeder der Decoder-bezogenen Klassen zu und übergeben Sie den Parameter Parameter verwenden.

  // 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);

Weisen Sie abschließend die Instanz DataBuffer zu. Die Zuweisung erfolgt über eine Fabrik. Zugriff über das Mitglied state_. Wir weisen einen Datenpuffer zu, der groß genug ist, ein einzelnes uint32_t, da dies die Größe des Anweisungsworts ist.

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

Destruktor – Definition

Der Destruktor ist einfach. Geben Sie einfach die Objekte kostenlos, die wir im Konstruktor zugewiesen haben. aber mit einem Twist. Da die Datenpufferinstanz als Referenz gezählt wird, Aus dem Aufrufen von delete für diesen Zeiger DecRef() wird das Objekt DecRef():

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

Methodendefinition

In unserem Fall ist die Implementierung dieser Methode ziemlich einfach. Wir gehen davon aus, dass die Adresse korrekt ausgerichtet ist und keine weitere Fehlerprüfung erforderlich.

Zuerst muss das Anweisungswort über den Speicher und der DataBuffer-Instanz.

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

Als Nächstes rufen wir die RiscVIEncoding-Instanz auf, um das Anweisungswort zu parsen. was vor dem Aufrufen des ISA-Decodierers selbst erledigt werden muss. Denken Sie daran, dass das ISA Der Decoder ruft die Instanz RiscVIEncoding direkt auf, um den Opcode abzurufen und Operanden, die durch das Anweisungswort angegeben werden. Das haben wir nicht verwenden, aber lassen Sie uns void ParseInstruction(uint32_t) als diese Methode verwenden.

  riscv_encoding_->ParseInstruction(iword);

Schließlich rufen wir den ISA-Decodierer auf und übergeben die Adresse und die Codierungsklasse.

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

Wenn du Hilfe brauchst oder deine Arbeit überprüfen möchtest, ist die Antwort ganz genau. hier.


Die Codierungsklasse

Die Codierungsklasse implementiert eine Schnittstelle, die von der Decoderklasse verwendet wird. um den Anweisungs-Opcode, seine Quell- und Zieloperanden zu erhalten, und Ressourcenoperanden. Diese Objekte hängen von Informationen aus dem Binärprogramm ab. Formatdecoder wie der Opcode, Werte bestimmter Felder im Anweisungswort usw. getrennt von der Decoder-Klasse, damit es nicht Codierungsunabhängig und ermöglicht die Unterstützung verschiedener Codierungsschemas zu erhalten.

RiscV32IEncodingBase ist eine abstrakte Klasse. Die Methoden, mit denen wir die in unserer abgeleiteten Klasse implementiert wurde, siehe unten.

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;
};

Auf den ersten Blick wirkt es etwas kompliziert, Parameter, aber für eine einfache Architektur wie RiscV ignorieren wir die meisten da ihre Werte impliziert werden.

Sehen wir uns die Methoden der Reihe nach durch.

OpcodeEnum GetOpcode(SlotEnum slot, int entry);

Die Methode GetOpcode gibt das Mitglied OpcodeEnum für den aktuellen Anweisungen, um den Anweisungs-Opcode zu identifizieren. Die OpcodeEnum-Klasse ist die in der generierten Isa-Decoderdatei riscv32i_enums.h definiert ist. Die Methode zwei Parameter, die für unsere Zwecke beide ignoriert werden können. Der erste von Das ist der Slottyp (eine enum-Klasse, die auch in riscv32i_enums.h definiert ist). Da RiscV nur einen einzigen Slot hat, hat nur ein möglicher Wert: SlotEnum::kRiscv32. Die zweite ist die Instanznummer des Slots (falls Es gibt mehrere Instanzen des Slots, was bei einigen VLIWs auftreten kann. Architekturen).

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);

Mit den nächsten beiden Methoden werden Hardwareressourcen im Prozessor modelliert um die Zyklusgenauigkeit zu verbessern. Für unsere Tutorial-Übungen verwenden wir Diese werden in der Implementierung also abgeschnitten und es wird nullptr zurückgegeben.

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);

Diese drei Methoden geben Zeiger auf Operand-Objekte zurück, die innerhalb von Die Semantikfunktionen für den Zugriff auf den Wert einer Anweisung Prädikatoperanden, alle Anweisungsquelloperanden und neue Werte schreiben -Werten an die Anweisungszieloperanden an. Da RiscV keine Anweisungsprädikaten muss diese Methode nur nullptr zurückgeben.

Das Muster der Parameter ist bei diesen Funktionen ähnlich. Erstens, genau wie bei GetOpcode: Der Slot und der Eintrag werden übergeben. Dann ist der Opcode für die Anweisung, für die der Operand erstellt werden muss. Dies wird nur verwendet, wenn der Parameter Unterschiedliche Opcodes müssen unterschiedliche Operandenobjekte für denselben Operanden zurückgeben. verwendet, was bei diesem RiscV-Simulator nicht der Fall ist.

Als Nächstes kommt der Eintrag „Predicate“, „Source“ und „Destination“ sowie die Aufzählung der Operanden, steht für den zu erstellenden Operanden. Diese stammen aus den drei OpEnums in riscv32i_enums.h wie unten gezeigt:

  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,
  };

Wenn Sie auf die riscv32.isa müssen Sie beachten, dass diese den Gruppen von Quelle und Ziel Operandennamen, die in der Deklaration der einzelnen Anweisungen verwendet werden. Durch die Verwendung unterschiedlicher Operandennamen für Operanden, die unterschiedliche Bitfelder und Operanden darstellen -Typen enthält, wird das Schreiben der Codierungsklasse erleichtert, da das enum-Mitglied eindeutig den genauen Operandentyp bestimmt, der zurückgegeben werden soll, und es ist nicht erforderlich, die Werte der Slot-, Eintrags- oder Opcode-Parameter.

Für Quell- und Zieloperanden ist schließlich die ordinale Position des wird der Operand übergeben (auch hier können wir ihn ignorieren) und für das Ziel Operand, die Latenz (in Zyklen), die zwischen dem Zeitpunkt der Anweisung ausgegeben wird und das Zielergebnis für nachfolgende Anweisungen verfügbar ist. In unserem Simulator ist diese Latenz 0, was bedeutet, dass die Anweisung das Ergebnis sofort an die Kasse weiterzugeben.

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

Die letzte Funktion wird verwendet, um die Latenz eines bestimmten Ziels zu erhalten. Operand, wenn er in der Datei .isa als * angegeben wurde. Das ist ungewöhnlich, und nicht für diesen RiscV-Simulator verwendet. Die Implementierung dieser Funktion gibt einfach 0 zurück.


Codierungsklasse definieren

Headerdatei (H)

Methoden

Öffnen Sie die Datei riscv32i_encoding.h. Alle erforderlichen Einschlussdateien haben wurde bereits hinzugefügt und die Namespaces wurden eingerichtet. Jegliche Codeergänzung ist Du folgst dem Kommentar // Exercise 2. jetzt nicht mehr

Beginnen wir mit der Definition einer Klasse RiscV32IEncoding, die von der generierten Schnittstelle.

class RiscV32IEncoding : public RiscV32IEncodingBase {
 public:

};

Als Nächstes sollte der Konstruktor einen Zeiger auf die Zustandsinstanz nehmen, in diesem Fall einen Zeiger auf riscv::RiscVState. Es sollte der Standard-Destruktor verwendet werden.

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

Bevor wir alle Oberflächenmethoden hinzufügen, fügen wir die Methode hinzu, RiscV32Decoder zum Parsen der Anweisung:

void ParseInstruction(uint32_t inst_word);

Als Nächstes fügen wir die Methoden hinzu, die einfache Überschreibungen haben. Namen der nicht verwendeten Parameter:

// 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; }

Fügen Sie schließlich die verbleibenden Methodenüberschreibungen der öffentlichen Schnittstelle hinzu, werden die Implementierungen auf die .cc-Datei zurückgestellt.


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;

Um die Implementierung der einzelnen Operanden-Getter-Methoden zu vereinfachen erstellen wir zwei Arrays mit callables (Funktionsobjekten), die vom numerischer Wert der Mitglieder SourceOpEnum bzw. DestOpEnum. Auf diese Weise wird der Text dieser zu -Methoden auf den Aufruf des Funktionsobjekt für den enum-Wert, der übergeben wird und seine Rückgabe zurückgibt Wert.

Um die Initialisierung dieser beiden Arrays zu organisieren, definieren wir zwei private -Methoden, die wie folgt vom Konstruktor aufgerufen werden:

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

Datenmitglieder

Folgende Datenmitglieder werden benötigt:

  • state_, um den Wert riscv::RiscVState * zu enthalten.
  • inst_word_ vom Typ uint32_t, die den Wert des aktuellen Anweisung.
  • opcode_, um den Opcode der aktuellen Anweisung zu enthalten, die durch die Methode ParseInstruction. Dies hat den Typ OpcodeEnum.
  • source_op_getters_ ist ein Array zum Speichern der Callables, die zum Abrufen der Quelle verwendet werden. Operandenobjekten. Der Typ der Array-Elemente ist absl::AnyInvocable<SourceOperandInterface *>()>
  • dest_op_getters_ ist ein Array zum Speichern der Callables, die zum Abrufen verwendet werden. Ziel-Operandenobjekte. Der Typ der Array-Elemente ist absl::AnyInvocable<DestinationOperandInterface *>()>
  • xreg_alias ist ein Array von ABI-Namen des RiscV-Ganzzahlregisters, z.B. „null“ und „ra“ statt "x0" und „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"};

Wenn du Hilfe brauchst oder deine Arbeit überprüfen möchtest, ist die Antwort ganz genau. hier.

Quelldatei (.cc).

Öffnen Sie die Datei riscv32i_encoding.cc. Alle erforderlichen Einschlussdateien haben wurde bereits hinzugefügt und die Namespaces wurden eingerichtet. Jegliche Codeergänzung ist Du folgst dem Kommentar // Exercise 2. jetzt nicht mehr

Hilfsfunktionen

Wir beginnen mit dem Schreiben einiger Hilfsfunktionen, mit denen wir Quell- und Zielregisteroperanden. Diese werden im registriert und ruft das RiscVState-Objekt auf, um ein Handle zum Register-Objekt und rufen anschließend eine Operand-Factory-Methode im Register-Objekt auf.

Beginnen wir mit den Ziel-Operandenhelfern:

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);
}

Wie Sie sehen, gibt es zwei Hilfsfunktionen. Die zweite erfordert eine zusätzliche Parameter op_name, der dem Operanden einen anderen Namen oder String ermöglicht als das zugrunde liegende Register.

Gleichermaßen für die Source Operand Helper:

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;
}

Konstruktor- und Schnittstellenfunktionen

Der Konstruktor und die Funktionen der Benutzeroberfläche sind sehr einfach. Der Konstruktor ruft einfach die beiden Initialisierungsmethoden auf, um die callables-Arrays für die Operanden des Operanden.

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

ParseInstruction speichert das Anweisungswort und dann den Opcode, erhält den Aufruf in den Binärdecoder-generierten Code.

// 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_);
}

Schließlich geben die Operand-Getter den Wert der von ihm aufgerufenen Getter-Funktion zurück. basierend auf der Arraysuche unter Verwendung des Enum-Werts "Destination/Source Operand".


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)]();
}

Array-Initialisierungsmethoden

Wie Sie sich vielleicht schon gedacht haben, besteht die meiste Arbeit darin, den Getter zu initialisieren. Arrays verwenden. Aber keine Sorge, dies geschieht mit einem einfachen, sich wiederholenden Muster. Lassen Sie uns mit InitializeDestinationOpGetters() beginnen, da es nur eine Zieloperanden.

Rufen Sie die generierte DestOpEnum-Klasse aus riscv32i_enums.h auf:

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

Für dest_op_getters_ müssen vier Einträge initialisiert werden, jeweils einen für kNone. kCsr, kNextPc und kRd. Der Einfachheit halber wird jeder Eintrag mit einem Lambda, obwohl Sie auch jede andere Form von Callable verwenden können. Die Signatur der Lambda-Funktion ist void(int latency).

Bisher haben wir noch nicht viel über die verschiedenen Arten von Reisezielen gesprochen. Operanden, die in MPACT-Sim definiert sind. Für diese Übung verwenden wir nur zwei Typen: generic::RegisterDestinationOperand definiert in register.h, und generic::DevNullOperand definiert in devnull_operand.h Die Details dieser Operanden sind derzeit nicht wirklich wichtig, außer dass der Parameter Ersteres wird zum Schreiben in Registern verwendet und Letzteres ignoriert alle Schreibvorgänge.

Der erste Eintrag für kNone ist einfach – es wird einfach ein nullptr zurückgegeben und optional einen Fehler protokollieren.

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

Es folgt kCsr. Hier geht es zum Schummeln. „Hello World“ Programm basiert nicht auf einer tatsächlichen CSR-Aktualisierung, sondern es gibt Boilerplate-Code, der CSR-Anweisungen ausführen. Die Lösung ist, dies einfach mithilfe eines reguläres Register namens „CSR“ und kanalisieren alle Schreibvorgänge in sie.

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

Als Nächstes kommt kNextPc, das sich auf den „PC“ bezieht. Es wird als Ziel verwendet. für alle Verzweigungen und Sprunganweisungen. Der Name ist in RiscVState definiert als kPcName.

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

Schließlich gibt es noch den Zieloperanden kRd. In riscv32i.isa der Operand rd wird nur verwendet, um auf das Ganzzahlregister zu verweisen, das in "rd" codiert ist. Feld des Anweisungsworts, sodass es keine Mehrdeutigkeit gibt, worauf es sich bezieht. Es ist nur eine Zusatzfunktion. Das Register x0 (ABI-Name zero) ist per Kabel mit 0 verbunden, Für dieses Register verwenden wir also DevNullOperand.

In diesem getter extrahieren wir zuerst den Wert im Feld rd mithilfe der Methode Extract-Methode, die aus der .bin_fmt-Datei generiert wurde. Wenn der Wert 0 ist, „DevNull“ zurückgeben sonst wird der richtige Register-Operand zurückgegeben, Achten Sie darauf, den entsprechenden Registrierungsalias als Operandennamen zu verwenden.

  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]);
    )
  }
}

Kommen wir nun zur Methode InitializeSourceOperandGetters(), bei der das Muster aber die Details unterscheiden sich geringfügig.

Werfen wir zuerst einen Blick auf die SourceOpEnum, die aus riscv32i.isa in der ersten Anleitung:

  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,
  };

Bei der Untersuchung der Mitglieder lassen sie sich zusätzlich zu kNone in zwei Gruppen unterteilen. Eins sind direkte Operanden: kBimm12, kImm12, kJimm20, kSimm12, kUimm20, und kUimm5. Die anderen sind die Registrierungs-Operanden: kCsr, kRs1 und kRs2.

Der Operand kNone wird genau wie Zieloperanden behandelt: Es wird ein nullptr.

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

Als Nächstes arbeiten wir an den Register-Operanden. Die kCsr werden ähnlich verarbeitet, wie die entsprechenden Zieloperanden gehandhabt wurden. Rufen Sie einfach Hilfsfunktion mit "CSR" als Registernamen ein.

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

Die Operanden kRs1 und kRs2 werden genauso behandelt wie kRd, mit der Ausnahme, dass x0 (oder zero wollten wir zwar nicht aktualisieren), möchten aber trotzdem sicherstellen, lesen wir immer 0 aus diesem Operanden. Dafür verwenden wir den generic::IntLiteralOperand<>-Klasse definiert in literal_operand.h Dieser Operand wird zum Speichern eines Literalwerts verwendet (im Gegensatz zu einem simulierten unmittelbaren Wert). Andernfalls ist das Muster das gleiche: Extrahieren Sie zuerst die rs1/rs2-Wert aus dem Anweisungswort; wenn dieser Wert null ist, wird das Literal zurückgegeben. Operand mit einem Vorlagenparameter 0, andernfalls wird ein reguläres Register zurückgegeben. Quelloperanden mithilfe der Hilfsfunktion und dem abi-Alias als Operanden Namen.

  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]);
  };

Schließlich verarbeiten wir die verschiedenen unmittelbaren Operanden. Unmittelbare Werte sind in Instanzen der Klasse generic::ImmediateOperand<> gespeichert, die definiert ist in immediate_operand.h Der einzige Unterschied zwischen den verschiedenen Getter für die unmittelbaren Operanden welche Extrahierer-Funktion verwendet wird und ob der Speichertyp signiert oder vorzeichenlos, laut Bitfield.

  // 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_));
  };
}

Wenn du Hilfe brauchst oder deine Arbeit überprüfen möchtest, ist die Antwort ganz genau. hier.

Damit ist diese Anleitung abgeschlossen. Wir hoffen, dass Ihnen diese Informationen weiterhelfen.