Mit dieser Anleitung werden folgende Ziele erreicht:
- Anleitung zum Implementieren einer Anweisungssemantik mit semantischen Funktionen
- Hier erfahren Sie, wie semantische Funktionen zur Beschreibung des ISA-Decodierers zusammenhängen.
- Schreibe semantische Funktionen für RiscV RV32I-Anweisungen.
- Fertigen Simulator testen, indem Sie ein kleines "Hello World"-Skript ausführen ausführbar sein.
Übersicht über semantische Funktionen
Eine semantische Funktion in MPACT-Sim ist eine Funktion, mit der die Operation implementiert wird. einer Anweisung so dargestellt, dass ihre Nebeneffekte im simulierten Zustand sichtbar sind auf die gleiche Weise, wie die Nebeneffekte der Anweisung sichtbar sind, wenn sie in Hardware. Die interne Darstellung der einzelnen decodierten Anweisungen im Simulator enthält ein Callable, mit dem die semantische Funktion für diese Anleitung.
Eine semantische Funktion hat die Signatur void(Instruction *)
, also eine
-Funktion, die einen Zeiger auf eine Instanz der Instruction
-Klasse
gibt void
zurück.
Die Klasse Instruction
ist definiert in
instruction.h
Im Zusammenhang mit dem Schreiben semantischen Funktionen sind insbesondere
die Schnittstellenvektoren für Quell- und Zieloperanden, auf die mithilfe der Methode
Aufrufe der Methoden Source(int i)
und Destination(int i)
.
Nachfolgend sind die Schnittstellen für Quell- und Ziel-Operanden dargestellt:
// The source operand interface provides an interface to access input values
// to instructions in a way that is agnostic about the underlying implementation
// of those values (eg., register, fifo, immediate, predicate, etc).
class SourceOperandInterface {
public:
// Methods for accessing the nth value element.
virtual bool AsBool(int index) = 0;
virtual int8_t AsInt8(int index) = 0;
virtual uint8_t AsUint8(int index) = 0;
virtual int16_t AsInt16(int index) = 0;
virtual uint16_t AsUint16(int) = 0;
virtual int32_t AsInt32(int index) = 0;
virtual uint32_t AsUint32(int index) = 0;
virtual int64_t AsInt64(int index) = 0;
virtual uint64_t AsUint64(int index) = 0;
// Return a pointer to the object instance that implements the state in
// question (or nullptr) if no such object "makes sense". This is used if
// the object requires additional manipulation - such as a fifo that needs
// to be pop'ed. If no such manipulation is required, nullptr should be
// returned.
virtual std::any GetObject() const = 0;
// Return the shape of the operand (the number of elements in each dimension).
// For instance {1} indicates a scalar quantity, whereas {128} indicates an
// 128 element vector quantity.
virtual std::vector<int> shape() const = 0;
// Return a string representation of the operand suitable for display in
// disassembly.
virtual std::string AsString() const = 0;
virtual ~SourceOperandInterface() = default;
};
// The destination operand interface is used by instruction semantic functions
// to get a writable DataBuffer associated with a piece of simulated state to
// which the new value can be written, and then used to update the value of
// the piece of state with a given latency.
class DestinationOperandInterface {
public:
virtual ~DestinationOperandInterface() = default;
// Allocates a data buffer with ownership, latency and delay line set up.
virtual DataBuffer *AllocateDataBuffer() = 0;
// Takes an existing data buffer, and initializes it for the destination
// as if AllocateDataBuffer had been called.
virtual void InitializeDataBuffer(DataBuffer *db) = 0;
// Allocates and initializes data buffer as if AllocateDataBuffer had been
// called, but also copies in the value from the current value of the
// destination.
virtual DataBuffer *CopyDataBuffer() = 0;
// Returns the latency associated with the destination operand.
virtual int latency() const = 0;
// Return a pointer to the object instance that implmements the state in
// question (or nullptr if no such object "makes sense").
virtual std::any GetObject() const = 0;
// Returns the order of the destination operand (size in each dimension).
virtual std::vector<int> shape() const = 0;
// Return a string representation of the operand suitable for display in
// disassembly.
virtual std::string AsString() const = 0;
};
Die grundlegende Art, eine semantische Funktion für einen normalen 3-Operanden zu schreiben
32-Bit-add
-Anweisung wie folgt:
void MyAddFunction(Instruction *inst) {
uint32_t a = inst->Source(0)->AsUint32(0);
uint32_t b = inst->Source(1)->AsUint32(0);
uint32_t c = a + b;
DataBuffer *db = inst->Destination(0)->AllocateDataBuffer();
db->Set<uint32_t>(0, c);
db->Submit();
}
Schauen wir uns die einzelnen Bestandteile dieser Funktion an. Die beiden ersten Zeilen der
Der Funktionstext liest aus den Quelloperanden 0 und 1. Der AsUint32(0)
-Aufruf
interpretiert die zugrunde liegenden Daten als uint32_t
-Array und ruft den 0.
-Elements. Dies gilt unabhängig davon, ob das zugrunde liegende Register oder der zugrunde liegende Wert
Arraywert oder nicht. Die Größe (in Elementen) des Quelloperanden kann
wird aus der Quelloperandenmethode shape()
abgerufen, die einen Vektor zurückgibt
der die Anzahl der Elemente
in jeder Dimension enthält. Diese Methode gibt {1}
zurück.
für einen Skalar, {16}
für einen Vektor mit 16 Elementen und {4, 4}
für ein 4 × 4-Array.
uint32_t a = inst->Source(0)->AsUint32(0);
uint32_t b = inst->Source(1)->AsUint32(0);
Dann wird der temporären uint32_t
mit dem Namen c
der Wert a + b
zugewiesen.
Für die nächste Zeile ist möglicherweise etwas mehr Erklärung erforderlich:
DataBuffer *db = inst->Destination(0)->AllocateDataBuffer();
Ein DataBuffer ist ein gezähltes Referenzobjekt, das zum Speichern von Werten in
simulierten Zustand wie Registern. Er ist relativ typisiert, hat jedoch einen
Größe basierend auf dem Objekt, von dem es zugewiesen ist. In diesem Fall ist die Größe
sizeof(uint32_t)
Diese Anweisung weist einen neuen Datenpuffer in der Größe für die
Das Ziel, das das Ziel dieses Zieloperanden ist – in diesem Fall ein
32-Bit-Ganzzahlregister. Der DataBuffer wird ebenfalls mit dem Parameter
Architekturlatenz für den Anweisungen. Dies wird während der Anweisung angegeben.
decodieren.
In der nächsten Zeile wird die Datenpufferinstanz als Array von uint32_t
und
schreibt den in c
gespeicherten Wert in das 0. Element.
db->Set<uint32_t>(0, c);
Mit der letzten Anweisung wird der Datenpuffer an den Simulator gesendet, der verwendet werden soll. als neuen Wert des Zielmaschinenstatus (in diesem Fall ein Register) nach dem Latenz des Befehls, der bei der Decodierung des Befehls festgelegt wurde, und der Zieloperandenvektor ausgefüllt.
Dies ist zwar eine relativ kurze Funktion, enthält aber einige Textbausteine. der sich bei der Implementierung einer Anweisung nach der Anweisung wiederholt. Außerdem kann dadurch die eigentliche Semantik der Anleitung verdeckt werden. Um um das Schreiben der semantischen Funktionen für die meisten Anweisungen zu vereinfachen, sind eine Reihe von helper-Funktionen in instruction_helpers.h Diese Helfer verbergen den Boilerplate-Code für Anweisungen mit ein, zwei oder drei Quelloperanden und einen einzelnen Zieloperanden. Werfen wir einen Blick auf zwei Operand-Hilfsfunktionen:
// This is a templated helper function used to factor out common code in
// two operand instruction semantic functions. It reads two source operands
// and applies the function argument to them, storing the result to the
// destination operand. This version supports different types for the result and
// each of the two source operands.
template <typename Result, typename Argument1, typename Argument2>
inline void BinaryOp(Instruction *instruction,
std::function<Result(Argument1, Argument2)> operation) {
Argument1 lhs = generic::GetInstructionSource<Argument1>(instruction, 0);
Argument2 rhs = generic::GetInstructionSource<Argument2>(instruction, 1);
Result dest_value = operation(lhs, rhs);
auto *db = instruction->Destination(0)->AllocateDataBuffer();
db->SetSubmit<Result>(0, dest_value);
}
// This is a templated helper function used to factor out common code in
// two operand instruction semantic functions. It reads two source operands
// and applies the function argument to them, storing the result to the
// destination operand. This version supports different types for the result
// and the operands, but the two source operands must have the same type.
template <typename Result, typename Argument>
inline void BinaryOp(Instruction *instruction,
std::function<Result(Argument, Argument)> operation) {
Argument lhs = generic::GetInstructionSource<Argument>(instruction, 0);
Argument rhs = generic::GetInstructionSource<Argument>(instruction, 1);
Result dest_value = operation(lhs, rhs);
auto *db = instruction->Destination(0)->AllocateDataBuffer();
db->SetSubmit<Result>(0, dest_value);
}
// This is a templated helper function used to factor out common code in
// two operand instruction semantic functions. It reads two source operands
// and applies the function argument to them, storing the result to the
// destination operand. This version requires both result and source operands
// to have the same type.
template <typename Result>
inline void BinaryOp(Instruction *instruction,
std::function<Result(Result, Result)> operation) {
Result lhs = generic::GetInstructionSource<Result>(instruction, 0);
Result rhs = generic::GetInstructionSource<Result>(instruction, 1);
Result dest_value = operation(lhs, rhs);
auto *db = instruction->Destination(0)->AllocateDataBuffer();
db->SetSubmit<Result>(0, dest_value);
}
Sie werden feststellen, dass statt einer Anweisung wie
uint32_t a = inst->Source(0)->AsUint32(0);
Die Hilfsfunktion verwendet:
generic::GetInstructionSource<Argument>(instruction, 0);
GetInstructionSource
ist eine Familie von vorlagenbasierten Hilfsfunktionen, die
werden verwendet, um vorlagenbasierte Zugriffsmethoden für die Anweisungsquelle bereitzustellen
Operanden. Ohne sie hätte jede Anweisung
spezialisiert für jeden Typ, um auf den Quelloperanden mit dem korrekten
As<int type>()
. Die Definitionen dieser Vorlage
Funktionen in
instruction.h
Wie Sie sehen, gibt es drei Implementierungen, je nachdem, ob die Quelle
Operandtypen sind mit dem Ziel identisch, unabhängig davon, ob das Ziel
oder ob sie alle unterschiedlich sind. Jede Version von
nimmt die Funktion einen Zeiger auf die Anweisungsinstanz sowie ein Callable
(einschließlich Lambda-Funktionen). Das bedeutet, dass add
jetzt neu geschrieben werden kann.
semantische Funktion oben so:
void MyAddFunction(Instruction *inst) {
generic::BinaryOp<uint32_t>(inst,
[](uint32_t a, uint32_t b) { return a + b; });
}
Bei Kompilierung mit bazel build -c opt
und copts = ["-O3"]
im Build
sollte die Datei ohne Mehraufwand vollständig inline eingefügt werden.
Kurzfassung und ohne Leistungseinbußen.
Wie bereits erwähnt, gibt es Hilfsfunktionen für unäre, binäre und ternäre Skalarfunktionen Anweisungen und Vektoräquivalente. Sie dienen auch als nützliche Vorlagen, .
Erster Build
Wenn Sie das Verzeichnis nicht in riscv_semantic_functions
geändert haben, tun Sie dies jetzt.
. Erstellen Sie dann das Projekt wie folgt – dieser Build sollte erfolgreich sein.
$ bazel build :riscv32i
...<snip>...
Es werden keine Dateien generiert, daher ist dies nur ein Probelauf, um sicherzustellen, dass alles in Ordnung ist.
Drei Operand-ALU-Anweisungen hinzufügen
Fügen wir nun die semantischen Funktionen für eine generische 3-Operanden-ALU hinzu.
Anleitung. Öffnen Sie die Datei rv32i_instructions.cc
und prüfen Sie, ob alle
Fehlende Definitionen werden der Datei rv32i_instructions.h
hinzugefügt.
Folgende Anweisungen werden hinzugefügt:
add
: Addition von 32-Bit-Ganzzahlen.and
: 32-Bit-Bitweise und.or
: 32-Bit-Bitweise oder.sll
: logische 32-Bit-Verschiebung nach links.sltu
: 32-Bit-Zeichen ohne Vorzeichen ist kleiner als.sra
: 32-Bit-arithmetische Verschiebung nach rechts.srl
: logische 32-Bit-Verschiebung nach rechts.sub
: 32-Bit-Ganzzahlsubtraktion.xor
: 32-Bit-Bitweises XOR.
Wenn Sie die vorherigen Anleitungen durchgegangen sind, erinnern Sie sich vielleicht, dass wir zwischen zwischen Anweisungen für Registrierung und sofortiger Registrierung des Decoders. Für semantische Funktionen ist das nicht mehr erforderlich. Die Operanden-Schnittstellen lesen den Operandenwert aus dem Operanden ist, registrieren oder unmittelbar, wobei die semantische Funktion völlig unabhängig von was der zugrunde liegende Quelloperanden ist.
Mit Ausnahme von sra
können alle oben genannten Anweisungen als Bedienelemente
Vorzeichenlose 32-Bit-Werte, also können wir dafür die Vorlagenfunktion BinaryOp
verwenden.
die wir zuvor mit nur dem
einzigen Vorlagentyp-Argument gesprochen haben. Füllen Sie die Felder
Funktionskörper in rv32i_instructions.cc
entsprechend. Beachten Sie, dass nur die niedrigen 5
Bits des zweiten Operanden für die Verschiebeanweisungen werden für die Verschiebung
Betrag. Andernfalls haben alle Vorgänge die Form src0 op src1
:
add
:a + b
and
:a & b
or
:a | b
sll
:a << (b & 0x1f)
sltu
:(a < b) ? 1 : 0
srl
:a >> (b & 0x1f)
sub
:a - b
xor
:a ^ b
Für sra
verwenden wir die Vorlage BinaryOp
mit drei Argumenten. Wenn Sie sich die
Vorlage ist das erste Typargument der Ergebnistyp uint32_t
. Das zweite ist
den Typ des Quelloperanden 0, in diesem Fall int32_t
, und der letzte ist der Typ
des Quelloperanden 1, in diesem Fall uint32_t
. Dadurch wird der Text von sra
Semantische Funktion:
generic::BinaryOp<uint32_t, int32_t, uint32_t>(
instruction, [](int32_t a, uint32_t b) { return a >> (b & 0x1f); });
Nehmen Sie die Änderungen vor und erstellen Sie den Build. Sie können Ihre Arbeit anhand rv32i_instructions.cc
Zwei Operand-ALU-Anweisungen hinzufügen
Es gibt nur zwei Anleitungen für 2-Operanden-ALU: lui
und auipc
. Erster
Kopiert den vorverschobenen Quelloperanden direkt in das Ziel. Letzteres
fügt die Anweisungsadresse zur unmittelbaren Angabe hinzu, bevor sie in den
Ziel. Die Anweisungsadresse kann über die Methode address()
aufgerufen werden
des Instruction
-Objekts.
Da es nur einen einzigen Quelloperanden gibt, kann BinaryOp
nicht verwendet werden.
müssen wir UnaryOp
verwenden. Da wir sowohl die Quelle als auch den
Zieloperanden als uint32_t
können wir die Vorlage für ein einzelnes Argument verwenden.
Version.
// This is a templated helper function used to factor out common code in
// single operand instruction semantic functions. It reads one source operand
// and applies the function argument to it, storing the result to the
// destination operand. This version supports the result and argument having
// different types.
template <typename Result, typename Argument>
inline void UnaryOp(Instruction *instruction,
std::function<Result(Argument)> operation) {
Argument lhs = generic::GetInstructionSource<Argument>(instruction, 0);
Result dest_value = operation(lhs);
auto *db = instruction->Destination(0)->AllocateDataBuffer();
db->SetSubmit<Result>(0, dest_value);
}
// This is a templated helper function used to factor out common code in
// single operand instruction semantic functions. It reads one source operand
// and applies the function argument to it, storing the result to the
// destination operand. This version requires that the result and argument have
// the same type.
template <typename Result>
inline void UnaryOp(Instruction *instruction,
std::function<Result(Result)> operation) {
Result lhs = generic::GetInstructionSource<Result>(instruction, 0);
Result dest_value = operation(lhs);
auto *db = instruction->Destination(0)->AllocateDataBuffer();
db->SetSubmit<Result>(0, dest_value);
}
Der Text der semantischen Funktion für lui
ist in etwa so einfach wie möglich.
geben Sie einfach die Quelle zurück. Mit der semantischen Funktion für auipc
wird eine kleinere
da Sie auf die Methode address()
in der Datei Instruction
Instanz. Die Antwort besteht darin, der Lambda-Aufnahme instruction
hinzuzufügen,
zur Verwendung im Lambda-Funktionsrumpf. Anstelle von [](uint32_t a) { ...
}
wie zuvor sollte die Lambda-Funktion [instruction](uint32_t a) { ... }
geschrieben werden.
Jetzt kann instruction
im Lambda-Text verwendet werden.
Nehmen Sie die Änderungen vor und erstellen Sie den Build. Sie können Ihre Arbeit anhand rv32i_instructions.cc
Anleitung zum Ändern des Kontrollflusses hinzufügen
Die zu implementierenden Änderungsanleitungen für den Kontrollfluss sind unterteilt. zu bedingten Zweiganweisungen (kürzere Zweige, die ausgeführt werden, wenn ein Vergleich wahr) und Jump-and-Link-Anweisungen, die verwendet werden, Funktionsaufrufe implementieren (und "-and-link" wird entfernt, indem der Link Register auf null setzen, sodass diese Schreibvorgänge No-Ops sind).
Bedingte Zweiganleitung hinzufügen
Es gibt keine Hilfsfunktion für Verzweigungen, daher gibt es zwei Optionen. Schreiben Sie die semantischen Funktionen von Grund auf oder eine lokale Hilfsfunktion. Da wir eine Anleitung mit 6 Zweigen implementieren müssen, scheint Letzteres die Hilfe zu lohnen. zu konzentrieren. Sehen wir uns vorher die Implementierung eines Zweigs an eine semantische Anweisung von Grund auf neu erstellen.
void MyConditionalBranchGreaterEqual(Instruction *instruction) {
int32_t a = generic::GetInstructionSource<int32_t>(instruction, 0);
int32_t b = generic::GetInstructionSource<int32_t>(instruction, 1);
if (a >= b) {
uint32_t offset = generic::GetInstructionSource<uint32_t>(instruction, 2);
uint32_t target = offset + instruction->address();
DataBuffer *db = instruction->Destination(0)->AllocateDataBuffer();
db->Set<uint32_t>(0,m target);
db->Submit();
}
}
Das Einzige, was je nach Anleitung variiert, ist der Zweig
und die Datentypen (vorzeichenbehaftet oder ohne Vorzeichen, 32-Bit-Ganzzahl) der beiden
Quelloperanden. Das bedeutet, dass wir einen Vorlagenparameter für die
Quelloperanden. Die Hilfsfunktion selbst muss die Instruction
und ein aufrufbares Objekt wie std::function
, das bool
zurückgibt
als Parameter. Die Hilfsfunktion würde so aussehen:
template <typename OperandType>
static inline void BranchConditional(
Instruction *instruction,
std::function<bool(OperandType, OperandType)> cond) {
OperandType a = generic::GetInstructionSource<OperandType>(instruction, 0);
OperandType b = generic::GetInstructionSource<OperandType>(instruction, 1);
if (cond(a, b)) {
uint32_t offset = generic::GetInstructionSource<uint32_t>(instruction, 2);
uint32_t target = offset + instruction->address();
DataBuffer *db = instruction->Destination(0)->AllocateDataBuffer();
db->Set<uint32_t>(0, target);
db->Submit();
}
}
Jetzt können wir die semantische Funktion bge
(vorzeichenbehafteter Zweig größer oder gleich) schreiben
als:
void RV32IBge(Instruction *instruction) {
BranchConditional<int32_t>(instruction,
[](int32_t a, int32_t b) { return a >= b; });
}
Im Folgenden sind die weiteren Anweisungen für den Zweig aufgeführt:
- Beq – Zweig gleich.
- Bgeu: Zweig größer oder gleich (ohne Vorzeichen).
- BLt - Zweig kleiner als (signiert).
- Bltu - Zweig kleiner als (unsigniert).
- Bne - Zweig ungleich.
Nehmen Sie die Änderungen vor, um diese semantischen Funktionen zu implementieren, neu aufbauen. Sie können Ihre Arbeit anhand rv32i_instructions.cc
Jump-and-Link-Anweisungen hinzufügen
Es macht keinen Sinn, eine Hilfsfunktion für den Jump und Link zu schreiben. und müssen sie komplett neu schreiben. Beginnen wir mit die Semantik der Unterricht zu überprüfen.
Die Anweisung jal
nimmt einen Versatz vom Quelloperanden 0 an und fügt ihn dem
Aktuellen PC (Anweisungsadresse) zum Berechnen des Jump-Ziels. Das Sprungziel
in den Zieloperanden 0 geschrieben. Die Rücksendeadresse ist die Adresse des
nächsten sequenziellen Anweisung. Er kann berechnet werden, indem der aktuelle Wert
in die Adresse einfügen. Die Rücksendeadresse wird an
Zieloperand 1. Denken Sie daran, den Zeiger des Anweisungsobjekts
der Lambda-Aufnahme.
Der jalr
-Befehl verwendet ein Basisregister als Quelloperand 0 und einen Offset
als Quelloperand 1 und addiert sie, um das Jump-Ziel zu berechnen.
Ansonsten entspricht sie der jal
-Anweisung.
Schreiben Sie basierend auf diesen Beschreibungen der Anweisungssemantik die beiden und Build. Sie können Ihre Arbeit anhand rv32i_instructions.cc
Anleitung für Speicher hinzufügen
Es gibt drei Speicheranweisungen, die wir implementieren müssen: Speicherbyte
(sb
), Halbwort speichern (sh
) und Wort speichern (sw
). Anleitung für Händler
unterscheiden sich von der bisher implementierten Anleitung dadurch,
in den lokalen Prozessorstatus schreiben. Stattdessen schreiben sie in eine Systemressource:
Hauptspeicher. MPACT-Sim behandelt den Speicher nicht als Befehlsoperanden,
Der Speicherzugriff muss
daher mit einer anderen Methode durchgeführt werden.
Die Antwort besteht darin, dem ArchState
-Objekt MPACT-Sim Speicherzugriffsmethoden hinzuzufügen.
Erstellen Sie ein neues RiscV-Statusobjekt, das von ArchState
abgeleitet ist.
in der dies hinzugefügt werden kann. Das ArchState
-Objekt verwaltet Kernressourcen wie
Registern und anderen Zustandsobjekten. Es verwaltet auch die Verspätungslinien,
die Zieloperandendaten zwischenspeichern, bis sie wieder geschrieben werden können.
die Register-Objekte. Die meisten Anweisungen können
ohne Wissen über
aber einige, wie z. B. Speichervorgänge und andere spezifische Systeme
Anweisungen erfordern eine Funktionalität, die sich in diesem Zustandsobjekt befindet.
Sehen wir uns die semantische Funktion für die fence
-Anweisung an,
bereits in rv32i_instructions.cc
als Beispiel implementiert. Das fence
Anweisung hält Anweisungsproblem zurück, bis bestimmte Speichervorgänge
abgeschlossen. Er wird verwendet, um die Speicherreihenfolge zwischen Anweisungen zu gewährleisten
vor der Anweisung und denen, die danach ausgeführt werden.
// Fence.
void RV32IFence(Instruction *instruction) {
uint32_t bits = instruction->Source(0)->AsUint32(0);
int fm = (bits >> 8) & 0xf;
int predecessor = (bits >> 4) & 0xf;
int successor = bits & 0xf;
auto *state = static_cast<RiscVState *>(instruction->state());
state->Fence(instruction, fm, predecessor, successor);
}
Der wichtigste Teil der semantischen Funktion der fence
-Anweisung sind die letzten beiden
Linien. Zuerst wird das Zustandsobjekt mit einer Methode im Instruction
abgerufen.
und downcast<>
auf die RiscV-spezifische abgeleitete Klasse. Dann der Fence
der Klasse RiscVState
wird aufgerufen, um den Fencing-Vorgang auszuführen.
Anweisungen für Händler funktionieren ähnlich. Erstens die effektive Adresse des
Der Speicherzugriff wird aus den Quelloperanden des Basis- und Offset-Befehls berechnet.
wird der zu speichernde Wert vom nächsten Quelloperanden abgerufen. Als Nächstes
Das RiscV-Statusobjekt wird über den state()
-Methodenaufruf abgerufen.
static_cast<>
, und die entsprechende Methode wird aufgerufen.
Die Methode StoreMemory
des RiscVState
-Objekts ist relativ einfach, hat aber einen
Folgen, die wir kennen müssen:
void StoreMemory(const Instruction *inst, uint64_t address, DataBuffer *db);
Wie Sie sehen, benötigt die Methode drei Parameter:
Anweisung selbst, die Adresse des Geschäfts und einen Verweis auf einen DataBuffer
Instanz, die die Geschäftsdaten enthält. Beachten Sie, dass keine Größe erforderlich ist, der
DataBuffer
-Instanz selbst enthält eine size()
-Methode. Es gibt jedoch keine
Zieloperanden, auf den die Anweisung zugreifen kann, die für
Ordnen Sie eine DataBuffer
-Instanz der entsprechenden Größe zu. Stattdessen müssen wir
DataBuffer
-Factory aus der Methode db_factory()
in
die Instanz Instruction
. Die Factory hat die Methode Allocate(int size)
das eine DataBuffer
-Instanz der erforderlichen Größe zurückgibt. Hier ist ein Beispiel
wie Sie damit eine DataBuffer
-Instanz für einen Halb-Wortspeicher zuweisen können
(Hinweis: auto
ist eine C++-Funktion, die den Typ von der rechten Hand ableitet.
Seite der Aufgabe):
auto *state = down_cast<RiscVState *>(instruction->state());
auto *db = state->db_factory()->Allocate(sizeof(uint16_t));
Sobald wir die DataBuffer
-Instanz haben, können wir wie gewohnt in sie schreiben:
db->Set<uint16_t>(0, value);
Übergeben Sie ihn dann an die Speicherspeicher-Schnittstelle:
state->StoreMemory(instruction, address, db);
Wir sind noch nicht ganz fertig. Die Instanz DataBuffer
wird als Referenz gezählt. Dieses
normalerweise von der Submit
-Methode verstanden und verarbeitet werden, damit die
Anwendungsfall so einfach wie möglich zu machen. StoreMemory
ist jedoch nicht
so geschrieben. Es IncRef
die DataBuffer
Instanz während des Betriebs
und dann DecRef
. Wenn die semantische Funktion jedoch
DecRef
als eigene Referenz verwendet, wird sie niemals zurückgefordert. Daher enthält die letzte Zeile
lautet:
db->DecRef();
Es gibt drei Speicherfunktionen. Der einzige Unterschied,
der sich unterscheidet, ist die Größe der
den Speicherzugriff. Das klingt nach einer großen Chance für einen anderen Ortsansässigen,
Vorlagen-Hilfsfunktion. Der einzige Unterschied bei der Funktion
Der Typ des Speicherwerts, daher muss die Vorlage diesen als Argument enthalten.
Ansonsten muss nur die Instanz Instruction
übergeben werden:
template <typename ValueType>
inline void StoreValue(Instruction *instruction) {
auto base = generic::GetInstructionSource<uint32_t>(instruction, 0);
auto offset = generic::GetInstructionSource<uint32_t>(instruction, 1);
uint32_t address = base + offset;
auto value = generic::GetInstructionSource<ValueType>(instruction, 2);
auto *state = down_cast<RiscVState *>(instruction->state());
auto *db = state->db_factory()->Allocate(sizeof(ValueType));
db->Set<ValueType>(0, value);
state->StoreMemory(instruction, address, db);
db->DecRef();
}
Fahren Sie fort, schließen Sie die semantischen Funktionen ab und erstellen Sie den Build. Sie können Ihre Gegen rv32i_instructions.cc
Anleitung für die Arbeitsspeicherauslastung hinzufügen
Die folgenden Ladeanweisungen müssen implementiert werden:
lb
: Lädt Byte, signierenerweitern in ein Wort.lbu
: Lädt Byte ohne Vorzeichen, nullerweitert in ein Wort.lh
: Ein halbes Wort wird geladen, durch Vorzeichen erweitert in ein Wort.lhu
: Ein halbes Wort ohne Vorzeichen wird mit Null-Erweiterung in ein Wort geladen.lw
– Wort laden.
Die Ladeanweisungen sind die komplexesten Anweisungen zum Modellieren.
dieser Anleitung. Sie ähneln den Anweisungen für das Geschäft:
Sie greifen auf das RiscVState
-Objekt zu, erhöhen jedoch die Komplexität, da jeder Ladevorgang
in zwei separate semantische Funktionen unterteilt. Die erste ist
ähnlich wie die Filialanweisung, da sie die effektive Adresse
und initiiert den Speicherzugriff. Die zweite wird ausgeführt, wenn der Speicher
und schreibt die Speicherdaten in das Register
Operand in das Feld.
Sehen wir uns zuerst die Deklaration der Methode LoadMemory
in RiscVState
an:
void LoadMemory(const Instruction *inst, uint64_t address, DataBuffer *db,
Instruction *child_inst, ReferenceCount *context);
Im Vergleich zur StoreMemory
-Methode benötigt LoadMemory
zwei zusätzliche
Parameter: ein Zeiger auf eine Instruction
-Instanz und ein Zeiger auf eine
Referenz gezähltes context
-Objekt Die erste ist die Anweisung child,
Implementiert das Register Write-Back (wie in der ISA-Decoder-Anleitung beschrieben). Es
Der Zugriff erfolgt über die Methode child()
in der aktuellen Instruction
-Instanz.
Letzteres ist ein Zeiger auf eine Instanz einer Klasse, die von
ReferenceCount
, die in diesem Fall eine DataBuffer
-Instanz speichert, die
die geladenen Daten enthalten. Das Kontextobjekt ist über die
Methode context()
im Instruction
-Objekt (für die meisten Anweisungen
ist auf nullptr
festgelegt).
Das Kontextobjekt für RiscV-Speicherlasten ist wie folgt definiert:
// A simple load context class for convenience.
struct LoadContext : public generic::ReferenceCount {
explicit LoadContext(DataBuffer *vdb) : value_db(vdb) {}
~LoadContext() override {
if (value_db != nullptr) value_db->DecRef();
}
// Override the base class method so that the data buffer can be DecRef'ed
// when the context object is recycled.
void OnRefCountIsZero() override {
if (value_db != nullptr) value_db->DecRef();
value_db = nullptr;
// Call the base class method.
generic::ReferenceCount::OnRefCountIsZero();
}
// Data buffers for the value loaded from memory (byte, half, word, etc.).
DataBuffer *value_db = nullptr;
};
Die Ladeanweisungen sind alle gleich, mit Ausnahme der Datengröße (Byte, Halbwort und Wort) und ob der geladene Wert vorzeichenerweitert ist oder nicht. Die Letztere berücksichtigt nur die child-Anweisung. Erstellen wir eine Vorlage Hilfsfunktion für die Anweisungen zum Hauptladen angezeigt. Sie ist dem Beispiel die Anweisung speichern, außer auf einen Quelloperanden, um einen Wert abzurufen, und erstellt ein Kontextobjekt.
template <typename ValueType>
inline void LoadValue(Instruction *instruction) {
auto base = generic::GetInstructionSource<uint32_t>(instruction, 0);
auto offset = generic::GetInstructionSource<uint32_t>(instruction, 1);
uint32_t address = base + offset;
auto *state = down_cast<RiscVState *>(instruction->state());
auto *db = state->db_factory()->Allocate(sizeof(ValueType));
db->set_latency(0);
auto *context = new riscv::LoadContext(db);
state->LoadMemory(instruction, address, db, instruction->child(), context);
context->DecRef();
}
Wie Sie sehen, besteht der Hauptunterschied darin, dass die zugewiesene DataBuffer
-Instanz
wird sowohl als Parameter an den LoadMemory
-Aufruf übergeben als auch im
LoadContext
-Objekt.
Die semantischen Funktionen der Anweisung child sind alle sehr ähnlich. Zunächst sollte der
LoadContext
wird durch Aufrufen der Instruction
-Methode context()
abgerufen.
statisch in die LoadContext *
umgewandelt. Zweitens wird der Wert (laut den Daten
Typ) wird aus der Load-Data-Instanz DataBuffer
gelesen. Drittens: Eine neue
Die Instanz DataBuffer
wird vom Zieloperanden zugewiesen. Die Funktion
geladener Wert wird in die neue DataBuffer
-Instanz geschrieben und Submit
übergeben.
Auch hier ist eine vorlagenbasierte Hilfsfunktion eine gute Idee:
template <typename ValueType>
inline void LoadValueChild(Instruction *instruction) {
auto *context = down_cast<riscv::LoadContext *>(instruction->context());
uint32_t value = static_cast<uint32_t>(context->value_db->Get<ValueType>(0));
auto *db = instruction->Destination(0)->AllocateDataBuffer();
db->Set<uint32_t>(0, value);
db->Submit();
}
Implementieren Sie nun die letzten Hilfsfunktionen und semantischen Funktionen. Bezahlen Achten Sie auf den Datentyp, den Sie in der Vorlage für jede Hilfsfunktion verwenden. und dass er der Größe und der Art der Last mit Vorzeichen und ohne Vorzeichen entspricht, Anleitung.
Sie können Ihre Arbeit anhand rv32i_instructions.cc
Endgültigen Simulator erstellen und ausführen
Jetzt können wir den endgültigen Simulator erstellen. Die
Top-Level-C++-Bibliotheken, die alle Arbeiten in diesen Tutorials zusammenfassen,
in other/
. Sie müssen sich diesen Code nicht zu intensiv ansehen. Mi.
dieses Thema in einer späteren Anleitung für Fortgeschrittene behandelt.
Ändern Sie Ihr Arbeitsverzeichnis in other/
und erstellen Sie den Build. Es sollte ohne
Fehler.
$ cd ../other
$ bazel build :rv32i_sim
In diesem Verzeichnis befindet sich ein einfaches „Hello World“- Programm in der Datei
hello_rv32i.elf
So führen Sie den Simulator für diese Datei aus und sehen sich die Ergebnisse an:
$ bazel run :rv32i_sim -- other/hello_rv32i.elf
Sie sollten etwas Ähnliches sehen:
INFO: Analyzed target //other:rv32i_sim (0 packages loaded, 0 targets configured).
INFO: Found 1 target...
Target //other:rv32i_sim up-to-date:
bazel-bin/other/rv32i_sim
INFO: Elapsed time: 0.203s, Critical Path: 0.00s
INFO: 1 process: 1 internal.
INFO: Build completed successfully, 1 total action
INFO: Running command line: bazel-bin/other/rv32i_sim other/hello_rv32i.elf
Starting simulation
Hello World
Simulation done
$
Der Simulator kann auch mit dem Befehl bazel
run :rv32i_sim -- -i other/hello_rv32i.elf
im interaktiven Modus ausgeführt werden. Daraufhin wird eine einfache
Command-Shell. Geben Sie help
in die Eingabeaufforderung ein, um die verfügbaren Befehle anzuzeigen.
$ bazel run :rv32i_sim -- -i other/hello_rv32i.elf
INFO: Analyzed target //other:rv32i_sim (0 packages loaded, 0 targets configured).
INFO: Found 1 target...
Target //other:rv32i_sim up-to-date:
bazel-bin/other/rv32i_sim
INFO: Elapsed time: 0.180s, Critical Path: 0.00s
INFO: 1 process: 1 internal.
INFO: Build completed successfully, 1 total action
INFO: Running command line: bazel-bin/other/rv32i_sim -i other/hello_rv32i.elf
_start:
80000000 addi ra, 0, 0
[0] > help
quit - exit command shell.
core [N] - direct subsequent commands to core N
(default: 0).
run - run program from current pc until a
breakpoint or exit. Wait until halted.
run free - run program in background from current pc
until breakpoint or exit.
wait - wait for any free run to complete.
step [N] - step [N] instructions (default: 1).
halt - halt a running program.
reg get NAME [FORMAT] - get the value or register NAME.
reg NAME [FORMAT] - get the value of register NAME.
reg set NAME VALUE - set register NAME to VALUE.
reg set NAME SYMBOL - set register NAME to value of SYMBOL.
mem get VALUE [FORMAT] - get memory from location VALUE according to
format. The format is a letter (o, d, u, x,
or X) followed by width (8, 16, 32, 64).
The default format is x32.
mem get SYMBOL [FORMAT] - get memory from location SYMBOL and format
according to FORMAT (see above).
mem SYMBOL [FORMAT] - get memory from location SYMBOL and format
according to FORMAT (see above).
mem set VALUE [FORMAT] VALUE - set memory at location VALUE(1) to VALUE(2)
according to FORMAT. Default format is x32.
mem set SYMBOL [FORMAT] VALUE - set memory at location SYMBOL to VALUE
according to FORMAT. Default format is x32.
break set VALUE - set breakpoint at address VALUE.
break set SYMBOL - set breakpoint at value of SYMBOL.
break VALUE - set breakpoint at address VALUE.
break SYMBOL - set breakpoint at value of SYMBOL.
break clear VALUE - clear breakpoint at address VALUE.
break clear SYMBOL - clear breakpoint at value of SYMBOL.
break clear all - remove all breakpoints.
help - display this message.
_start:
80000000 addi ra, 0, 0
[0] >
Damit ist diese Anleitung abgeschlossen. Wir hoffen, dass Ihnen diese Informationen weiterhelfen.