Mit dieser Anleitung werden folgende Ziele erreicht:
- Hier erfahren Sie, wie Anweisungen im MPACT-Sim-Simulator dargestellt werden.
- Informationen zur Struktur und Syntax der ISA-Beschreibungsdatei
- ISA-Beschreibungen für die RiscV RV32I-Anweisungen schreiben
Übersicht
In MPACT-Sim werden die Zielanweisungen decodiert und in einer internen Darstellung gespeichert, um ihre Informationen besser verfügbar zu machen und die Semantik schneller zu ausführen. Diese Anweisungsinstanzen werden in einem Anweisungscache zwischengespeichert, um die Anzahl der Ausführungen häufig ausgeführter Anweisungen zu reduzieren.
Instruktionskurs
Bevor wir beginnen, sollten wir uns ein wenig ansehen, wie Anweisungen in MPACT-Sim dargestellt werden. Die Klasse Instruction
ist in mpact-sim/mpact/sim/generic/instruction.h definiert.
Die Instanz der Instruction-Klasse enthält alle Informationen, die zur Simulation der Anweisung bei ihrer "Ausführung" erforderlich sind. Zum Beispiel:
- Anweisungsadresse, simulierte Anweisungsgröße, d. h. Größe in .text.
- Opcode der Anweisung.
- Zeiger auf die Schnittstelle des Prädikatsoperanden (falls zutreffend)
- Vektor von Zeiger auf Quelloperand-Schnittstellen.
- Vektor von Zieloperand-Schnittstellenzeigern.
- Semantische Funktion aufrufbar.
- Verweis auf das Objekt für den Gebäudestatus.
- Verweis auf das Kontextobjekt.
- Verweis auf untergeordnete und nächste Anweisungsinstanzen.
- Deaktivierungsstring.
Diese Instanzen werden im Allgemeinen in einem Anweisungscache (Instanz-) gespeichert und immer dann verwendet, wenn der Befehl noch einmal ausgeführt wird. Dadurch wird die Leistung während der Laufzeit verbessert.
Mit Ausnahme des Zeigers auf das Kontextobjekt werden alle von dem Anweisungsdekodierer ausgefüllt, der aus der ISA-Beschreibung generiert wird. Für diese Anleitung müssen Sie keine Details zu diesen Elementen kennen, da wir sie nicht direkt verwenden werden. Stattdessen reicht ein allgemeines Verständnis davon, wie sie verwendet werden.
Die aufrufbare semantische Funktion ist das C++-Funktions-/Methoden-/Funktionsobjekt (einschließlich Lambdas), das die Semantik der Anweisung implementiert. Bei einer add
-Anweisung wird beispielsweise jeder Quelloperanden geladen, die beiden Operanden hinzugefügt und das Ergebnis in einen einzelnen Zieloperanden geschrieben. Das Thema semantische Funktionen wird in der Anleitung zu semantischen Funktionen ausführlich behandelt.
Anweisungsoperanden
Die Anweisungsklasse enthält Verweise auf drei Arten von Operandenschnittstellen: Prädikat, Quelle und Ziel. Mit diesen Schnittstellen können semantische Funktionen unabhängig vom tatsächlichen Typ des zugrunde liegenden Befehlsoperators geschrieben werden. So erfolgt beispielsweise der Zugriff auf die Werte von Registern und Immediaten über dieselbe Schnittstelle. Das bedeutet, dass Anweisungen, die denselben Vorgang ausführen, aber auf verschiedenen Operanden (z. B. Register im Vergleich zu Immediaten), mit derselben semantischen Funktion implementiert werden können.
Die Schnittstelle für Prädikatoperanden wird bei ISAs, die die Ausführung von Anweisungen mit Prädikat unterstützen, verwendet, um anhand des booleschen Werts des Prädikats zu bestimmen, ob eine bestimmte Anweisung ausgeführt werden soll. Bei anderen ISAs ist sie null.
// The predicte operand interface is intended primarily as the interface to
// read the value of instruction predicates. It is separated from source
// predicates to avoid mixing it in with the source operands needed for modeling
// the instruction semantics.
class PredicateOperandInterface {
public:
virtual bool Value() = 0;
// Return a string representation of the operand suitable for display in
// disassembly.
virtual std::string AsString() const = 0;
virtual ~PredicateOperandInterface() = default;
};
Über die Quelloperand-Schnittstelle kann die Anweisungssemantische Funktion Werte aus den Anweisungsoperanden lesen, ohne Rücksicht auf den zugrunde liegenden Operandentyp. Die Schnittstellenmethoden unterstützen sowohl skalare als auch vektorwertige Operanden.
// 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;
};
Die Zieloperand-Schnittstelle bietet Methoden zum Zuweisen und Verwalten von DataBuffer
-Instanzen (der interne Datentyp zum Speichern von Registerwerten). Mit einem Zieloperanden ist auch eine Latenz verbunden. Das ist die Anzahl der Zyklen, die gewartet werden müssen, bis der Wert des Zielregisters mit der von der Anweisungssemantischen Funktion zugewiesenen Datenpufferinstanz aktualisiert wird. Die Latenz einer add
-Anweisung kann beispielsweise 1 sein, während sie für eine mpy
-Anweisung 4 sein kann. Dies wird in der Anleitung zu semantischen Funktionen ausführlicher behandelt.
// 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;
};
ISA-Beschreibung
Die ISA- (Instruction Set Architecture) eines Prozessors definiert das abstrakte Modell, durch das Software mit der Hardware interagiert. Sie definiert die verfügbaren Anweisungen, die Datentypen, Register und den anderen Maschinenstatus, auf den die Anweisungen angewendet werden, sowie ihr Verhalten (Semantik). Für MPACT-Sim enthält das ISA nicht die eigentliche Codierung der Anweisungen. Dies wird separat behandelt.
Die ISA des Prozessors wird in einer Beschreibungsdatei ausgedrückt, die den Befehlssatz auf einer abstrakten, codierungsunabhängigen Ebene beschreibt. In der Beschreibungsdatei werden die verfügbaren Anleitungen aufgelistet. Für jede Anweisung müssen der Name, die Anzahl und die Namen der Operanden sowie die Bindung an eine C++-Funktion bzw. -Aufrufbar aufgelistet werden, die ihre Semantik implementiert. Außerdem kann ein Formatierungsstring für die Deassemblage und die Verwendung von Hardwareressourcennamen in der Anweisung angegeben werden. Ersteres ist nützlich, um eine textuelle Darstellung der Anweisung für das Debuggen, Tracing oder die interaktive Verwendung zu erstellen. Mit letzterem können Sie die Zyklusgenauigkeit der Simulation erhöhen.
Die ISA-Beschreibungsdatei wird vom isa-Parser geparst, der Code für den darstellungsunabhängigen Anweisungsdecoder generiert. Dieser Decoder ist für das Ausfüllen der Felder der Anweisungsobjekte verantwortlich. Die spezifischen Werte, z. B. die Zielregisternummer, werden aus einem formatspezifischen Befehlsdekodierer abgerufen. Einer dieser Decoder ist der Binärdecodierer, auf den sich die nächste Anleitung bezieht.
In dieser Anleitung wird beschrieben, wie Sie eine ISA-Beschreibungsdatei für eine einfache, skalare Architektur schreiben. Zur Veranschaulichung verwenden wir eine Teilmenge des RiscV RV32I-Befehlssatzes und erstellen zusammen mit den anderen Anleitungen einen Simulator, der ein „Hello World“-Programm simulieren kann. Weitere Informationen zum RiscV-ISA findest du in den Risc-V-Spezifikationen.
Öffnen Sie zuerst die Datei:
riscv_isa_decoder/riscv32i.isa
Der Inhalt der Datei ist in mehrere Abschnitte unterteilt. Erstens die ISA-Erklärung:
isa RiscV32I {
namespace mpact::sim::codelab;
slots { riscv32; }
}
Dadurch wird RiscV32I
als Name der ISA deklariert und der Codegenerator erstellt eine Klasse namens RiscV32IEncodingBase
, die die Schnittstelle definiert, über die der generierte Decoder Opcode- und Operandeninformationen abrufen kann. Der Name dieser Klasse wird generiert, indem der ISA-Name in die Pascal-Schreibweise konvertiert und dann mit EncodingBase
verkettet wird. Die Deklaration slots { riscv32; }
gibt an, dass im RiscV32I-ISA nur ein einziger Anweisungsslot riscv32
(im Gegensatz zu mehreren Slots in einer VLIW-Anweisung) vorhanden ist und dass die einzigen gültigen Anweisungen die sind, die in riscv32
ausgeführt werden sollen.
// First disasm fragment is 15 char wide and left justified.
disasm widths = {-15};
Damit wird angegeben, dass das erste Demontagefragment einer Dissasembly-Spezifikation (siehe unten) in einem 15 Zeichen breiten Feld linksbündig wird. Alle nachfolgenden Fragmente werden diesem Feld ohne zusätzliches Leerzeichen angehängt.
Darunter befinden sich drei Slot-Deklarationen: riscv32i
, zicsr
und riscv32
.
Gemäß der obigen isa
-Definition sind nur Anweisungen, die für den Slot riscv32
definiert sind, Teil der RiscV32I
-ISa. Wozu dienen die anderen beiden Slots?
Mit Slots können Anweisungen in separate Gruppen unterteilt werden, die dann am Ende in einem einzigen Slot kombiniert werden können. Beachten Sie die Schreibweise : riscv32i, zicsr
in der riscv32
-Steckplatzdeklaration. Hiermit wird festgelegt, dass Steckplatz riscv32
alle Anweisungen erbt, die in den Steckplätzen zicsr
und riscv32i
definiert sind. Die RiscV-32-Bit-ISA besteht aus einer Basis-ISA namens RV32I, der eine Reihe optionaler Erweiterungen hinzugefügt werden kann. Mit dem Slot-Mechanismus können die Anweisungen in diesen Erweiterungen separat angegeben und dann nach Bedarf kombiniert werden, um die gesamte ISA zu definieren. In diesem Fall sind die Anweisungen in der RiscV-Gruppe "I" getrennt von denen in der Gruppe "zicsr" definiert. Für die gewünschte finale RiscV-ISA können zusätzliche Gruppen für „M“ (Multiplikation/Division), „F“ (Gleitkomma mit einfacher Genauigkeit), „D“ (Gleitkomma mit doppelter Genauigkeit), „C“ (kompakte 16‑Bit-Anweisungen) usw. definiert werden.
// The RiscV 'I' instructions.
slot riscv32i {
...
}
// RiscV32 CSR manipulation instructions.
slot zicsr {
...
}
// The final instruction set combines riscv32i and zicsr.
slot riscv32 : riscv32i, zicsr {
...
}
Die Slotdefinitionen für zicsr
und riscv32
müssen nicht geändert werden. In dieser Anleitung liegt der Schwerpunkt jedoch darauf, dem riscv32i
-Steckplatz die erforderlichen Definitionen hinzuzufügen. Sehen wir uns an, was derzeit in dieser Anzeigenfläche definiert ist:
// The RiscV 'I' instructions.
slot riscv32i {
// Include file that contains the declarations of the semantic functions for
// the 'I' instructions.
includes {
#include "learning/brain/research/mpact/sim/codelab/riscv_semantic_functions/solution/rv32i_instructions.h"
}
// These are all 32 bit instructions, so set default size to 4.
default size = 4;
// Model these with 0 latency to avoid buffering the result. Since RiscV
// instructions have sequential semantics this is fine.
default latency = 0;
// The opcodes.
opcodes {
fence{: imm12 : },
semfunc: "&RV32IFence"c
disasm: "fence";
ebreak{},
semfunc: "&RV32IEbreak",
disasm: "ebreak";
}
}
Zuerst gibt es einen Abschnitt includes {}
mit den Headerdateien, die in den generierten Code aufgenommen werden müssen, wenn auf diesen Slot direkt oder indirekt in der endgültigen ISA verwiesen wird. Einschlussdateien können auch in einem includes {}
-Abschnitt mit globalem Geltungsbereich aufgelistet werden. In diesem Fall sind sie immer enthalten. Dies kann praktisch sein, wenn sonst die gleiche Include-Datei jeder Slotdefinition hinzugefügt werden müsste.
Die Deklarationen default size
und default latency
definieren, dass die Größe eines Befehls 4 ist, sofern nicht anders angegeben, und dass die Latenz eines Schreibvorgangs für den Zieloperanden 0 Zyklen beträgt. Die hier angegebene Größe der Anweisung ist die Größe des Programmzähler-Inkrementwerts, um die Adresse der nächsten sequenziellen Anweisung zu berechnen, die im simulierten Prozessor ausgeführt werden soll. Diese Größe entspricht möglicherweise nicht der Größe in Byte der Anweisungsdarstellung in der Eingabedatei.
Im Mittelpunkt der Slot-Definition steht der Opcode-Abschnitt. Wie Sie sehen, wurden in riscv32i
bisher nur zwei Anweisungen (Opcodes) definiert: fence
und ebreak
. Der Opcode fence
wird definiert, indem der Name (fence
) und die Operandenspezifikation ({: imm12 : }
) angegeben werden, gefolgt vom optionalen Disassemblierungsformat ("fence"
) und dem Callable, das als semantische Funktion gebunden werden soll ("&RV32IFence"
).
Die Anweisungsoperanden werden als Dreifach angegeben, wobei jede Komponente durch ein Semikolon getrennt wird. predicate ':' source operand list ':' destination operand list. Die Quell- und Zieloperandenlisten sind durch Kommas getrennte Listen von Operandennamen. Wie Sie sehen, enthalten die Anweisungsoperanden für die Anweisung fence
keine Prädikatoperanden, nur einen einzelnen Quell-Operandennamen imm12
und keine Zieloperanden. Der RiscV-RV32I-Untersatz unterstützt keine vordefinierte Ausführung. Daher ist der Prädikatoperand in dieser Anleitung immer leer.
Die semantische Funktion wird als String angegeben, der zum Angeben der C++-Funktion oder des aufrufbaren Objekts erforderlich ist, mit dem die semantische Funktion aufgerufen werden soll. Die Signatur der semantischen Funktion/Aufrufbaren ist void(Instruction *)
.
Die Deaktivierungsspezifikation besteht aus einer durch Kommas getrennten Liste von Strings.
Normalerweise werden nur zwei Strings verwendet: eine für den Opcode und eine für die Operanden. Bei der Formatierung (mit dem Aufruf AsString()
in der Anleitung) wird jeder String in einem Feld gemäß der oben beschriebenen disasm widths
-Spezifikation formatiert.
Mit den folgenden Übungen können Sie der Datei riscv32i.isa
Anweisungen hinzufügen, die ausreichen, um ein „Hello World“-Programm zu simulieren. Für alle, die es eilig haben, finden Sie die Lösungen in riscv32i.isa und rv32i_instructions.h.
Ersten Build ausführen
Falls Sie das Verzeichnis noch nicht in riscv_isa_decoder
geändert haben, tun Sie dies jetzt. Erstellen Sie dann das Projekt wie unten beschrieben. Dieser Build sollte erfolgreich sein.
$ cd riscv_isa_decoder
$ bazel build :all
Wechseln Sie nun zurück zum Stammverzeichnis des Repositorys und sehen wir uns die generierten Quellen an. Wechseln Sie dazu zum Verzeichnis bazel-out/k8-fastbuild/bin/riscv_isa_decoder
. Angenommen, Sie befinden sich auf einem x86-Host. Bei anderen Hosts ist „k8-fastbuild“ ein anderer String.
$ cd ..
$ cd bazel-out/k8-fastbuild/bin/riscv_isa_decoder
In diesem Verzeichnis befinden sich unter anderem die folgenden generierten C++-Dateien:
riscv32i_decoder.h
riscv32i_decoder.cc
riscv32i_enums.h
riscv32i_enums.cc
Sehen wir uns riscv32i_enums.h
an. Klicken Sie dazu im Browser darauf. Sie sollten sehen, dass er in etwa Folgendes enthält:
#ifndef RISCV32I_ENUMS_H
#define RISCV32I_ENUMS_H
namespace mpact {
namespace sim {
namespace codelab {
enum class SlotEnum {
kNone = 0,
kRiscv32,
};
enum class PredOpEnum {
kNone = 0,
kPastMaxValue = 1,
};
enum class SourceOpEnum {
kNone = 0,
kCsr = 1,
kImm12 = 2,
kRs1 = 3,
kPastMaxValue = 4,
};
enum class DestOpEnum {
kNone = 0,
kCsr = 1,
kRd = 2,
kPastMaxValue = 3,
};
enum class OpcodeEnum {
kNone = 0,
kCsrs = 1,
kCsrsNw = 2,
kCsrwNr = 3,
kEbreak = 4,
kFence = 5,
kPastMaxValue = 6
};
constexpr char kNoneName[] = "none";
constexpr char kCsrsName[] = "Csrs";
constexpr char kCsrsNwName[] = "CsrsNw";
constexpr char kCsrwNrName[] = "CsrwNr";
constexpr char kEbreakName[] = "Ebreak";
constexpr char kFenceName[] = "Fence";
extern const char *kOpcodeNames[static_cast<int>(
OpcodeEnum::kPastMaxValue)];
enum class SimpleResourceEnum {
kNone = 0,
kPastMaxValue = 1
};
enum class ComplexResourceEnum {
kNone = 0,
kPastMaxValue = 1
};
enum class AttributeEnum {
kPastMaxValue = 0
};
} // namespace codelab
} // namespace sim
} // namespace mpact
#endif // RISCV32I_ENUMS_H
Wie Sie sehen, ist jeder Slot, Opcode und Operand, der in der Datei riscv32i.isa
definiert wurde, in einem der Aufzählungstypen definiert. Außerdem gibt es ein OpcodeNames
-Array, in dem alle Namen der Opcodes gespeichert sind (es ist in riscv32i_enums.cc
definiert). Die anderen Dateien enthalten den generierten Decoder, der in einer anderen Anleitung ausführlicher behandelt wird.
Bazel-Buildregel
Das ISA-Dekodierungsziel in Bazel wird mit einem benutzerdefinierten Regelmakro namens mpact_isa_decoder
definiert, das aus mpact/sim/decoder/mpact_sim_isa.bzl
im mpact-sim
-Repository geladen wird. Für diese Anleitung ist das in riscv_isa_decoder/BUILD
definierte Build-Ziel:
mpact_isa_decoder(
name = "riscv32i_isa",
src = "riscv32i.isa",
includes = [],
isa_name = "RiscV32I",
deps = [
"//riscv_semantic_functions:riscv32i",
],
)
Diese Regel ruft das ISA-Parser-Tool und den -Generator auf, um den C++-Code zu generieren. Dann kompiliert die generierte Datei in eine Bibliothek, von der andere Regeln abhängen können, indem das Label //riscv_isa_decoder:riscv32i_isa
verwendet wird. Im Abschnitt includes
werden zusätzliche .isa
-Dateien angegeben, die die Quelldatei enthalten kann. Mit isa_name
wird angegeben, welche spezifische isa in der Quelldatei, für die der Decoder generiert werden soll, verwendet werden soll, falls mehr als eine angegeben ist.
ALU-Anweisungen für Registrierung und Registrierung hinzufügen
Jetzt ist es an der Zeit, der Datei riscv32i.isa
einige neue Anweisungen hinzuzufügen. Die erste Gruppe von Anweisungen sind Register-Register-ALU-Anweisungen wie add
, and
usw. Bei RiscV32 wird für diese Anweisungen das binäre Anweisungsformat vom Typ R verwendet:
31.25 | 24..20 | 19.15 | 14.12 | 11..7 | 6..0 |
---|---|---|---|---|---|
7 | 5 | 5 | 3 | 5 | 7 |
func7 | rs2 | rs1 | func3 | 3. | Opcode |
Die .isa
-Datei wird zwar zum Generieren eines formatunabhängigen Decoders verwendet, es ist aber dennoch hilfreich, das Binärformat und sein Layout zu berücksichtigen, um die Einträge zu steuern. Wie Sie sehen, gibt es drei Felder, die für den Decodierer relevant sind, der die Anweisungsobjekte ausfüllt: rs2
, rs1
und rd
. An dieser Stelle werden wir diese Namen für auf dieselbe Weise codierte Ganzzahlregister (Bitsequenzen) verwenden, und zwar in allen Anweisungsfeldern in denselben Anweisungsfeldern.
Wir fügen folgende Anweisungen hinzu:
add
: Ganzzahl hinzufügen.and
– Bitweises AND.or
– das bitweise oder.sll
– logische Linksverschiebung.sltu
: Kleiner als, nicht signiert.sub
: Ganzzahlsubtrahieren.xor
– Bitweises XOR.
Jede dieser Anweisungen wird dem Abschnitt opcodes
der riscv32i
-Steckplatzdefinition hinzugefügt. Wir müssen für jede Anweisung den Namen, die Opcodes, die Deassemblage und die semantische Funktion angeben. Der Name ist einfach. Verwenden wir einfach die oben genannten Opcode-Namen. Außerdem werden für alle dieselben Operanden verwendet, sodass wir { : rs1, rs2 : rd}
für die Operandenangabe verwenden können. Das bedeutet, dass der vom Register rs1 angegebene Quelloperand im Quelloperandenvektor im Anweisungsobjekt den Index 0 hat, der vom Register rs2 angegebene Quelloperand den Index 1 und der vom Register rd angegebene Zieloperand das einzige Element im Zieloperandenvektor (mit dem Index 0) ist.
Als Nächstes folgt die Spezifikation der semantischen Funktion. Dazu werden das Schlüsselwort semfunc
und ein C++ String verwendet, der ein Callable angibt, das für die Zuweisung zu einem std::function
-Objekt verwendet werden kann. In dieser Anleitung verwenden wir Funktionen. Der aufrufbare String ist also "&MyFunctionName"
. Wenn Sie das in der fence
-Anweisung vorgeschlagene Namensschema verwenden, sollten diese "&RV32IAdd"
, "&RV32IAnd"
usw. sein.
Und zuletzt die Spezifikation zum Auseinanderbauen. Sie beginnt mit dem Keyword disasm
und wird von einer durch Kommas getrennten Liste von Strings gefolgt, die angeben, wie die Anweisung als String ausgegeben werden soll. Das Zeichen %
vor einem Operandennamen weist auf eine Stringersetzung mithilfe der Stringdarstellung dieses Operanden hin. Für die Anweisung add
wäre das disasm: "add", "%rd,
%rs1,%rs2"
. Der Eintrag für die add
-Anweisung sollte also so aussehen:
add{ : rs1, rs2 : rd},
semfunc: "&RV32IAdd",
disasm: "add", "%rd, %rs1, %rs2";
Bearbeiten Sie die Datei riscv32i.isa
und fügen Sie der Beschreibung von .isa
alle diese Anweisungen hinzu. Wenn Sie Hilfe benötigen oder Ihre Arbeit überprüfen möchten, finden Sie die vollständige Beschreibungsdatei hier.
Nachdem die Anleitung zur Datei riscv32i.isa
hinzugefügt wurde, müssen für jede der neuen semantischen Funktionen, die auf die Datei rv32i_instructions.h
in „../semantic_functions/“ verwiesen wurden, Funktionsdeklarationen hinzugefügt werden. Wenn Sie Hilfe benötigen oder Ihre Arbeit überprüfen möchten, finden Sie hier die Antwort.
Wenn alles erledigt ist, wechseln Sie wieder in das Verzeichnis riscv_isa_decoder
und erstellen Sie den Build neu. Sie können sich die generierten Quelldateien gern ansehen.
ALU-Anweisungen mit Sofortnachrichten hinzufügen
Als Nächstes fügen wir ALU-Anweisungen hinzu, die anstelle eines der Register einen unmittelbaren Wert verwenden. Es gibt drei Gruppen dieser Anweisungen (basierend auf dem unmittelbaren Feld): die I-Type-Direktanweisungen mit einem 12-Bit-Direktbefehl mit Vorzeichen, die spezialisierten I-Type-Anweisungen für die Verschiebung und die U-Type-Anweisung mit einem 20-Bit-Direktwert ohne Vorzeichen. Folgende Formate sind verfügbar:
Das I-Type-Format unmittelbar:
31..20 | 19.15 | 14.12 | 11..7 | 6.0 |
---|---|---|---|---|
12 | 5 | 3 | 5 | 7 |
imm12 | rs1 | func3 | 3. | Opcode |
Das spezielle I-Typ-Direktformat:
31.25 | 24..20 | 19.15 | 14.12 | 11..7 | 6..0 |
---|---|---|---|---|---|
7 | 5 | 5 | 3 | 5 | 7 |
func7 | uimm5 | rs1 | func3 | 3. | Opcode |
U-Typ-Sofortformat:
31.12 | 11.7 | 6..0 |
---|---|---|
20 | 5 | 7 |
uimm20 | rd | Opcode |
Wie Sie sehen, beziehen sich die Operandennamen rs1
und rd
auf dieselben Bitfelder wie zuvor und werden verwendet, um Ganzzahlregister darzustellen. Diese Namen können also beibehalten werden. Die Felder für unmittelbare Werte haben unterschiedliche Länge und Position. Zwei davon (uimm5
und uimm20
) sind vorzeichenlos, während imm12
ein vorzeichenbehafteter Wert ist. Jedes von ihnen hat einen eigenen Namen.
Die Operanden für die I-Typ-Anweisungen sollten daher { : rs1, imm12 :rd
}
sein. Für die speziellen I-Type-Anweisungen sollte { : rs1, uimm5 : rd}
lauten.
Die U-Typ-Anweisungsoperandenangabe sollte { : uimm20 : rd }
sein.
Folgende I-Type-Anweisungen müssen hinzugefügt werden:
addi
: Sofort hinzufügen.andi
– Bitweises AND mit Immediate.ori
– Bitweise oder mit Immediate-Wertxori
: Bitweises xor mit sofortiger Wirkung.
Die speziellen I-Typ-Anweisungen, die wir hinzufügen müssen, sind:
slli
: Sofortige Verschiebung nach links.srai
– Arithmetische Rechtsverschiebung um einen unmittelbaren Wert.srli
– Logische Rechtsverschiebung um einen sofortigen Wert.
Die Anweisungen für den U-Typ, die wir hinzufügen müssen, sind:
auipc
: Fügen Sie dem PC sofort obere Elemente hinzu.lui
: Oberer Wert wird sofort geladen.
Die Namen für die Opcodes ergeben sich ganz natürlich aus den obigen Anweisungsnamen. Sie müssen keine neuen Namen erfinden, da sie alle eindeutig sind. Denken Sie bei der Angabe der semantischen Funktionen daran, dass die Anweisungsobjekte Schnittstellen zu den Quelloperanden codieren, die unabhängig vom zugrunde liegenden Operandentyp sind. Das bedeutet, dass Anweisungen mit derselben Operation, die sich jedoch in den Operandentypen unterscheiden können, dieselbe semantische Funktion haben können. Beispielsweise führt die Anweisung addi
dieselbe Operation wie die Anweisung add
aus, wenn der Operandentyp ignoriert wird. Daher kann dieselbe semantische Funktionsspezifikation "&RV32IAdd"
verwendet werden. Gleiches gilt für andi
, ori
, xori
und slli
.
Für die anderen Anweisungen werden neue semantische Funktionen verwendet, die aber nach dem Vorgang und nicht nach den Operanden benannt werden sollten. Verwenden Sie für srai
also "&RV32ISra"
. Die U-Typ-Anweisungen auipc
und lui
haben keine Registeräquivalente. Daher können "&RV32IAuipc"
und "&RV32ILui"
verwendet werden.
Die Strings zum Trennen sind den Strings in der vorherigen Übung sehr ähnlich. Verweise auf %rs2
werden jedoch wie zu erwarten durch %imm12
, %uimm5
oder %uimm20
ersetzt.
Nehmen Sie die Änderungen vor und führen Sie einen Build aus. Prüfen Sie die generierte Ausgabe. Wie bisher können Sie Ihre Arbeit mit riscv32i.isa und rv32i_instructions.h abgleichen.
Branch- und Jump-and-Link-Anweisungen hinzufügen
Die zu fügenden Sprung- und Jump-and-Link-Anweisungen verwenden beide einen Zieloperanden, der nur in der Anweisung selbst impliziert ist, nämlich den nächsten PC-Wert. An dieser Stelle wird dies als ein richtiger Operand mit dem Namen next_pc
behandelt. Er wird in einer späteren Anleitung näher definiert.
Anleitung für Zweigstellen
Die hinzugefügten Äste verwenden alle die B-Typ-Codierung.
31 | 30–25 | 24..20 | 19.15 | 14.12 | 11..8 | 7 | 6..0 |
---|---|---|---|---|---|---|---|
1 | 6 | 5 | 5 | 3 | 4 | 1 | 7 |
imm | imm | rs2 | rs1 | func3 | imm | imm | Opcode |
Die verschiedenen Direktfelder werden zu einem vorzeichenbehafteten 12-Bit-Direktwert verkettet. Da das Format nicht wirklich relevant ist, nennen wir diese Immediate-Anweisung bimm12
, für 12‑Bit-Branch Immediate. Die Fragmentierung wird in der nächsten Anleitung zum Erstellen des Binärdecodierers behandelt. Alle Zweiganweisungen vergleichen die von rs1 und rs2 angegebenen Ganzzahlregister. Wenn die Bedingung wahr ist, wird der sofortige Wert zum aktuellen PC-Wert addiert, um die Adresse der nächsten auszuführenden Anweisung zu erzeugen. Die Operanden für die Zweiganweisungen sollten daher { : rs1, rs2, bimm12 : next_pc }
sein.
Die Filialanweisungen, die wir hinzufügen müssen, sind:
beq
– Verzweigung bei Gleichheit.bge
– Verzweigung, wenn größer oder gleichbgeu
– Verzweigung, wenn größer oder gleich (unsigniert).blt
– Zweig, wenn kleiner als.bltu
– Verzweigung, wenn der Wert kleiner als der Wert ohne Vorzeichen ist.bne
– Verzweigung bei „Nicht gleich“.
Diese Opcodenamen sind alle eindeutig und können daher in der .isa
-Beschreibung wiederverwendet werden. Natürlich müssen neue Namen für semantische Funktionen hinzugefügt werden, z. B.
"&RV32IBeq"
usw.
Die Deassemblagespezifikation ist jetzt etwas komplizierter, da die Adresse der Anweisung zum Berechnen des Ziels verwendet wird, ohne dass sie tatsächlich Teil der Anweisungsoperanden ist. Da sie jedoch Teil der im Anweisungsobjekt gespeicherten Informationen sind, stehen sie zur Verfügung. Die Lösung besteht darin, die Ausdruckssyntax im Zerlegungsstring zu verwenden. Anstatt „%“ gefolgt vom Operandennamen zu verwenden, können Sie auch %(expression: print format) eingeben. Es werden nur sehr einfache Ausdrücke unterstützt. Eine davon ist aber Adresse plus Offset, wobei das @
-Symbol für die aktuelle Anweisungsadresse verwendet wird. Das Druckformat ähnelt printf-Formaten im C-Stil, allerdings ohne das vorangestellte %
. Das Deassemblage-Format für die beq
-Anweisung sieht dann so aus:
disasm: "beq", "%rs1, %rs2, %(@+bimm12:08x)"
Anleitung für Jump-and-Link
Es müssen nur zwei Jump-and-Link-Anweisungen hinzugefügt werden: jal
(Jump-and-Link) und jalr
(indirektes Jump-and-Link).
Für die jal
-Anweisung wird die J-Typ-Codierung verwendet:
31 | 30..21 | 20 | 19.12 | 11..7 | 6.0 |
---|---|---|---|---|---|
1 | 10 | 1 | 8 | 5 | 7 |
imm | imm | imm | imm | rd | Opcode |
Genau wie bei den Sprunganweisungen ist der 20‑Bit-Immediate-Wert auf mehrere Felder verteilt. Wir nennen ihn jimm20
. Die Fragmentierung ist zu diesem Zeitpunkt nicht wichtig, wird aber in der nächsten Anleitung zum Erstellen des Binärdecodierers behandelt. Die Operandenangabe lautet dann { : jimm20 : next_pc, rd }
. Beachten Sie, dass es zwei Zieloperanden gibt: den nächsten PC-Wert und das in der Anweisung angegebene Link-Register.
Ähnlich wie bei den oben genannten Verzweigungsanweisungen wird das Deaktivierungsformat so:
disasm: "jal", "%rd, %(@+jimm20:08x)"
Der indirekte Sprung mit Verknüpfung verwendet das I-Typ-Format mit dem 12‑Bit-Immediate-Wert. Dabei wird dem durch rs1
angegebenen Ganzzahlregister der signaturerweiterte Immediate-Wert hinzugefügt, um die Zielbefehlsadresse zu generieren. Das Linkregister ist das durch rd
angegebene Ganzzahlregister.
31..20 | 19.15 | 14.12 | 11..7 | 6.0 |
---|---|---|---|---|
12 | 5 | 3 | 5 | 7 |
imm12 | rs1 | func3 | 3. | Opcode |
Wenn Sie das Muster sehen, würden Sie jetzt daraus ableiten, dass die Operandenspezifikation für jalr
{ : rs1, imm12 : next_pc, rd }
lauten sollte, und die Disassembly-Spezifikation:
disasm: "jalr", "%rd, %rs1, %imm12"
Nehmen Sie die Änderungen vor und erstellen Sie dann den Build. Prüfen Sie die generierte Ausgabe. Wie bereits zuvor können Sie Ihre Arbeit mit riscv32i.isa und rv32i_instructions.h vergleichen.
Anleitung zum Geschäft hinzufügen
Die Anleitung zum Shop ist sehr einfach. Sie alle verwenden das S-Typ-Format:
31–25 | 24..20 | 19.15 | 14.12 | 11..7 | 6..0 |
---|---|---|---|---|---|
7 | 5 | 5 | 3 | 5 | 7 |
imm | rs2 | rs1 | func3 | imm | Opcode |
Wie Sie sehen, ist dies ein weiterer Fall einer fragmentierten 12-Bit-Sofortversion. Wir nennen sie simm12
. Bei den Speicheranweisungen wird der Wert des von rs2 angegebenen Ganzzahlregisters in der effektiven Adresse im Speicher gespeichert, die durch Addition des Werts des von rs1 angegebenen Ganzzahlregisters zum signerweiterten Wert des 12‑Bit-Immediate-Werts ermittelt wird. Das Operandenformat sollte für alle Speicheranweisungen { : rs1, simm12, rs2 }
sein.
Folgende Anleitungen müssen implementiert werden:
sb
– Byte speichern.sh
: Halbwort speichern.sw
– Wort speichern.
Die Zerlegungsspezifikation für sb
sieht so aus, wie Sie es erwarten würden:
disasm: "sb", "%rs2, %simm12(%rs1)"
Die Spezifikationen der semantischen Funktionen sind auch zu erwarten: "&RV32ISb"
usw.
Nehmen Sie die Änderungen vor und führen Sie dann den Build aus. Prüfen Sie die generierte Ausgabe. Sie können Ihre Arbeit wie zuvor mit riscv32i.isa und rv32i_instructions.h prüfen.
Anweisungen zum Laden hinzufügen
Ladeanweisungen werden im Simulator etwas anders modelliert als andere Anweisungen. Um Fälle zu modellieren, in denen die Ladelatenz ungewiss ist, werden Ladeanweisungen in zwei separate Aktionen unterteilt: 1) Berechnung der effektiven Adresse und Speicherzugriff und 2) Ergebnisrückschreiben. Im Simulator wird dazu die semantische Aktion der Ladung in zwei separate Anweisungen unterteilt: die Hauptanweisung und eine untergeordnete Anweisung. Wenn wir Operanden angeben, müssen wir diese sowohl für die Haupt- als auch für die child-Anweisung angeben. Dazu wird die Operandenangabe als Liste von Dreingaben behandelt. Die Syntax lautet:
{(predicate : sources : destinations),
(predicate : sources : destinations), ... }
Die Ladeanweisungen verwenden alle das I-Typ-Format wie viele der vorstehenden Anweisungen:
31..20 | 19.15 | 14.12 | 11..7 | 6.0 |
---|---|---|---|---|
12 | 5 | 3 | 5 | 7 |
imm12 | rs1 | func3 | 3. | Opcode |
Die Operandenspezifikation teilt die Operanden auf, die zum Berechnen der Adresse und zum Initiieren des Speicherzugriffs vom Registerziel für die Ladedaten erforderlich sind: {( : rs1, imm12 : ), ( : : rd) }
.
Da die semantische Aktion auf zwei Anweisungen aufgeteilt ist, müssen auch die semantischen Funktionen zwei aufrufbare Elemente angeben. Für lw
(Ladewort) würde das so geschrieben:
semfunc: "&RV32ILw", "&RV32ILwChild"
Die Vorgabe für das Auseinanderbauen ist konventioneller. Von der Unterweisung
wird keine Erwähnung genommen. Für lw
sollte Folgendes gelten:
disasm: "lw", "%rd, %imm12(%rs1)"
Folgende Ladeanweisungen müssen implementiert werden:
lb
– Ladebyte.lbu
– Ladebefehl für ein Byte ohne Vorzeichen.lh
– Halbwort laden.lhu
: Halbwort wird unvorzeichenlos geladen.lw
– Wort laden.
Nehmen Sie die Änderungen vor und führen Sie dann den Build aus. Prüfen Sie die generierte Ausgabe. Wie bereits zuvor können Sie Ihre Arbeit mit riscv32i.isa und rv32i_instructions.h vergleichen.
Vielen Dank, dass Sie so weit gekommen sind. Wir hoffen, dass Ihnen diese Informationen weiterhelfen.