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 Wertriscv::RiscVState *
zu enthalten.inst_word_
vom Typuint32_t
, die den Wert des aktuellen Anweisung.opcode_
, um den Opcode der aktuellen Anweisung zu enthalten, die durch die MethodeParseInstruction
. Dies hat den TypOpcodeEnum
.source_op_getters_
ist ein Array zum Speichern der Callables, die zum Abrufen der Quelle verwendet werden. Operandenobjekten. Der Typ der Array-Elemente istabsl::AnyInvocable<SourceOperandInterface *>()>
dest_op_getters_
ist ein Array zum Speichern der Callables, die zum Abrufen verwendet werden. Ziel-Operandenobjekte. Der Typ der Array-Elemente istabsl::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 ®_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 ®_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.