Les objectifs de ce tutoriel sont les suivants:
- Découvrez comment les instructions sont représentées dans le simulateur MPACT-Sim.
- Découvrez la structure et la syntaxe du fichier de description ISA.
- Écrire les descriptions de l'ISA pour le sous-ensemble d'instructions RV32I de RiscV
Présentation
Dans MPACT-Sim, les instructions cibles sont décodées et stockées dans une représentation interne pour rendre leurs informations plus disponibles et accélérer l'exécution de la sémantique. Ces instances d'instructions sont mises en cache dans un cache d'instructions afin de réduire le nombre d'exécutions des instructions fréquemment exécutées.
Classe d'instructions
Avant de commencer, il est utile de voir comment les instructions sont représentées dans MPACT-Sim. La classe Instruction
est définie dans mpact-sim/mpact/sim/generic/instruction.h.
L'instance de la classe Instruction contient toutes les informations nécessaires pour simuler l'instruction lorsqu'elle est "exécutée", par exemple :
- Adresse d'instruction, taille d'instruction simulée, c'est-à-dire taille dans .text.
- Code opération de l'instruction.
- Pointeur d'interface d'opérande de prédicat (le cas échéant)
- Vecteur de pointeurs d'interface d'opérande source.
- Vecteur de pointeurs d'interface d'opérande de destination.
- Fonction sémantique appelable.
- Pointeur vers l'objet d'état architectural.
- Pointeur vers l'objet de contexte.
- Pointeur vers les instances enfant et suivante d'instruction.
- Chaîne de démontage.
Ces instances sont généralement stockées dans un cache d'instructions (instance) et réutilisées chaque fois que l'instruction est réexécutée. Cela améliore les performances pendant l'exécution.
À l'exception du pointeur vers l'objet de contexte, tous sont renseignés par le décodeur d'instructions généré à partir de la description de l'ISA. Pour ce tutoriel, il n'est pas nécessaire de connaître les détails de ces éléments, car nous ne les utiliserons pas directement. Il suffit de comprendre de manière générale comment elles sont utilisées.
La fonction sémantique appelable est l'objet fonction/méthode/fonction C++ (y compris les lambdas) qui implémente la sémantique de l'instruction. Par exemple, pour une instruction add
, elle charge chaque opérande source, ajoute les deux opérandes et écrit le résultat dans un seul opérande de destination. Le sujet des fonctions sémantiques est abordé en détail dans le tutoriel sur les fonctions sémantiques.
Opérandes d'instruction
La classe d'instruction comprend des pointeurs vers trois types d'interfaces d'opérande : prédicat, source et destination. Ces interfaces permettent d'écrire des fonctions sémantiques indépendamment du type réel de l'opérande d'instruction sous-jacent. Par exemple, l'accès aux valeurs des registres et des résultats immédiats se fait via la même interface. Cela signifie que les instructions qui effectuent la même opération, mais sur des opérandes différents (par exemple, registres et instances immédées) peuvent être mises en œuvre à l'aide de la même fonction sémantique.
L'interface d'opérande de prédicat, pour les ISA compatibles avec l'exécution d'instructions conditionnelles (pour les autres ISA, elle est nulle), permet de déterminer si une instruction donnée doit s'exécuter en fonction de la valeur booléenne du prédicat.
// 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;
};
L'interface d'opérande source permet à la fonction sémantique d'instruction de lire les valeurs des opérandes d'instructions sans tenir compte du type d'opérande sous-jacent. Les méthodes d'interface acceptent les opérandes à valeurs scalaires et vectorielles.
// 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;
};
L'interface d'opérande de destination fournit des méthodes d'allocation et de gestion des instances DataBuffer
(type de données interne utilisé pour stocker les valeurs de registre). Une operande de destination est également associée à une latence, qui correspond au nombre de cycles d'attente jusqu'à ce que l'instance de tampon de données allouée par la fonction sémantique de l'instruction soit utilisée pour mettre à jour la valeur du registre cible. Par exemple, la latence d'une instruction add
peut être de 1, tandis que pour une instruction mpy
, elle peut être égale à 4. Ce point est abordé plus en détail dans le tutoriel sur les fonctions sémantiques.
// 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;
};
Description ISA
L'architecture d'ensemble d'instructions (ISA, Instruction Set Architecture) d'un processeur définit le modèle abstrait par lequel le logiciel interagit avec le matériel. Il définit l'ensemble des instructions disponibles, les types de données, les registres et les autres états de machine sur lesquels les instructions s'exécutent, ainsi que leur comportement (sémantique). Pour les besoins de MPACT-Sim, l'ISA n'inclut pas l'encodage réel des instructions. Ces données sont traitées séparément.
L'ISA du processeur est exprimée dans un fichier de description qui décrit l'ensemble d'instructions à un niveau abstrait et indépendant de l'encodage. Le fichier de description énumère l'ensemble des instructions disponibles. Pour chaque instruction, il est obligatoire de lister son nom, le nombre et les noms de ses opérandes, ainsi que son association à une fonction/fonction appelable C++ qui implémente sa sémantique. Vous pouvez également spécifier une chaîne de mise en forme de démontage et l'utilisation par l'instruction des noms de ressources matérielles. Le premier est utile pour générer une représentation textuelle de l'instruction à des fins de débogage, de traçage ou d'utilisation interactive. Ce dernier peut être utilisé pour intégrer une précision de cycle plus élevée dans la simulation.
Le fichier de description ISA est analysé par l'analyseur isa, qui génère du code pour le décodeur d'instructions indépendant de la représentation. Ce décodeur est chargé de renseigner les champs des objets d'instruction. Les valeurs spécifiques, par exemple le numéro de registre de destination, sont obtenues à partir d'un décodeur d'instructions spécifique au format. Le décodeur binaire en est un, et il sera l'objet du prochain tutoriel.
Ce tutoriel explique comment écrire un fichier de description de l'ISA pour une architecture scalaire simple. Pour illustrer cela, nous allons utiliser un sous-ensemble de l'ensemble d'instructions RiscV RV32I et, avec les autres tutoriels, créer un simulateur capable de simuler un programme "Hello World". Pour en savoir plus sur la norme ISA RiscV, consultez les spécifications Risc-V.
Commencez par ouvrir le fichier :
riscv_isa_decoder/riscv32i.isa
Le contenu du fichier est divisé en plusieurs sections. La première est la déclaration de l'ISA :
isa RiscV32I {
namespace mpact::sim::codelab;
slots { riscv32; }
}
Cela déclare RiscV32I
comme nom de l'ISA et que le générateur de code crée une classe appelée RiscV32IEncodingBase
qui définit l'interface que le décodeur généré utilisera pour obtenir des informations sur le code d'opération et les opérandes. Le nom de cette classe est généré en convertissant le nom ISA en casse Pascal, puis en le concaténant avec EncodingBase
. La déclaration slots { riscv32; }
spécifie qu'il n'y a qu'un seul emplacement d'instruction riscv32
dans l'ISA RiscV32I (par opposition à plusieurs emplacements dans une instruction VLIW), et que les seules instructions valides sont celles définies pour s'exécuter dans riscv32
.
// First disasm fragment is 15 char wide and left justified.
disasm widths = {-15};
Cela spécifie que le premier fragment de désassemblage de toute spécification de désassemblage (voir plus loin) sera justifié à gauche dans un champ de 15 caractères de large. Tous les fragments suivants seront ajoutés à ce champ sans espace supplémentaire.
En dessous, trois déclarations de slots : riscv32i
, zicsr
et riscv32
.
D'après la définition isa
ci-dessus, seules les instructions définies pour l'emplacement riscv32
feront partie de l'isa RiscV32I
. À quoi servent les deux autres emplacements ?
Les emplacements peuvent être utilisés pour regrouper des instructions en groupes distincts, qui peuvent ensuite être combinés en un seul emplacement à la fin. Notez la notation : riscv32i, zicsr
dans la déclaration d'emplacement riscv32
. Cela indique que l'emplacement riscv32
hérite de toutes les instructions définies dans les emplacements zicsr
et riscv32i
. L'ISA 32 bits RiscV se compose d'une ISA de base appelée RV32I, à laquelle un ensemble d'extensions facultatives peut être ajouté. Le mécanisme d'emplacement permet de spécifier séparément les instructions de ces extensions, puis de les combiner à la fin si nécessaire pour définir l'ISA globale. Dans ce cas, les instructions du groupe "I" de RiscV sont définies séparément de celles du groupe "zicsr". Des groupes supplémentaires peuvent être définis pour "M" (multiplier/diviser), "F" (à virgule flottante à simple précision), "D" (à virgule flottante à double précision), "C" (instructions compactes 16 bits), etc. si nécessaire pour l'ISA RiscV finale souhaitée.
// The RiscV 'I' instructions.
slot riscv32i {
...
}
// RiscV32 CSR manipulation instructions.
slot zicsr {
...
}
// The final instruction set combines riscv32i and zicsr.
slot riscv32 : riscv32i, zicsr {
...
}
Il n'est pas nécessaire de modifier les définitions des emplacements zicsr
et riscv32
. Toutefois, l'objectif de ce tutoriel est d'ajouter les définitions nécessaires à l'emplacement riscv32i
. Examinons de plus près ce qui est actuellement défini dans cet emplacement :
// 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";
}
}
Tout d'abord, une section includes {}
répertorie les fichiers d'en-tête à inclure dans le code généré lorsque cet emplacement est référencé, directement ou indirectement, dans l'ISA finale. Les fichiers d'inclusion peuvent également être listés dans une section includes {}
de portée globale, auquel cas ils sont toujours inclus. Cela peut être utile si le même fichier d'inclusion doit être ajouté à chaque définition d'emplacement.
Les déclarations default size
et default latency
définissent que, sauf indication contraire, la taille d'une instruction est de 4, et que la latence d'écriture d'un opérande de destination est de 0 cycle. Notez que la taille de l'instruction spécifiée ici correspond à la taille de l'incrément du compteur de programme pour calculer l'adresse de l'instruction séquentielle suivante à exécuter dans le processeur simulé. Cette valeur peut être identique ou non à la taille en octets de la représentation de l'instruction dans le fichier exécutable d'entrée.
La section "OPcode" se trouve au centre de la définition de l'emplacement. Comme vous pouvez le voir, seuls deux codes d'opération (instructions) fence
et ebreak
ont été définis jusqu'à présent dans riscv32i
. L'opcode fence
est défini en spécifiant le nom (fence
) et la spécification de l'opérande ({: imm12 : }
), suivi du format de désassemblage facultatif ("fence"
) et de l'appelable à lier en tant que fonction sémantique ("&RV32IFence"
).
Les opérandes d'instruction sont spécifiés sous la forme d'un triple, chaque composant étant séparé par un point-virgule : predicate ':' source operand list ':'
destination operand list. Les listes d'opérandes source et de destination sont des listes de noms d'opérandes séparés par une virgule. Comme vous pouvez le constater, les opérandes d'instruction de l'instruction fence
ne contiennent aucun opérande de prédicat, un seul nom d'opérande source imm12
et aucun opérande de destination. Le sous-ensemble RiscV RV32I n'est pas compatible avec l'exécution prédite. Par conséquent, l'opérande de prédicat sera toujours vide dans ce tutoriel.
La fonction sémantique est spécifiée en tant que chaîne nécessaire pour spécifier la fonction C++ ou en tant que chaîne appelable à utiliser pour appeler la fonction sémantique. La signature de la fonction sémantique/appelable est void(Instruction *)
.
La spécification de désassemblage se compose d'une liste de chaînes séparées par une virgule.
En règle générale, seules deux chaînes sont utilisées, l'une pour l'opcode et l'autre pour les opérandes. Lorsqu'elle est formatée (à l'aide de l'appel AsString()
dans l'instruction), chaque chaîne est formatée dans un champ conformément à la spécification disasm widths
décrite ci-dessus.
Les exercices suivants vous aident à ajouter au fichier riscv32i.isa
des instructions suffisantes pour simuler un programme "Hello World". Pour ceux pressés, les solutions sont disponibles dans riscv32i.isa et rv32i_instructions.h.
Effectuer la compilation initiale
Si vous n'avez pas remplacé le répertoire par riscv_isa_decoder
, faites-le maintenant. Ensuite, compilez le projet comme suit. Cette compilation devrait aboutir.
$ cd riscv_isa_decoder
$ bazel build :all
Revenez maintenant à la racine du dépôt dans votre répertoire, puis examinons les sources générées. Pour cela, remplacez le répertoire par bazel-out/k8-fastbuild/bin/riscv_isa_decoder
(en supposant que vous êtes sur un hôte x86. Pour les autres hôtes, k8-fastbuild sera une autre chaîne).
$ cd ..
$ cd bazel-out/k8-fastbuild/bin/riscv_isa_decoder
Dans ce répertoire, vous trouverez, entre autres, les fichiers C++ générés suivants :
riscv32i_decoder.h
riscv32i_decoder.cc
riscv32i_enums.h
riscv32i_enums.cc
Examinons riscv32i_enums.h
en cliquant dessus dans le navigateur. Il doit contenir un contenu semblable à celui-ci:
#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
Comme vous pouvez le voir, chaque emplacement, code d'opération et opérande défini dans le fichier riscv32i.isa
est défini dans l'un des types d'énumération. De plus, il existe un tableau OpcodeNames
qui stocke tous les noms des opcodes (il est défini dans riscv32i_enums.cc
). Les autres fichiers contiennent le décodeur généré, qui sera abordé plus en détail dans un autre tutoriel.
Règle de compilation Bazel
La cible du décodeur ISA dans Bazel est définie à l'aide d'une macro de règle personnalisée appelée mpact_isa_decoder
, qui est chargée à partir de mpact/sim/decoder/mpact_sim_isa.bzl
dans le dépôt mpact-sim
. Pour ce tutoriel, la cible de compilation définie dans riscv_isa_decoder/BUILD
est:
mpact_isa_decoder(
name = "riscv32i_isa",
src = "riscv32i.isa",
includes = [],
isa_name = "RiscV32I",
deps = [
"//riscv_semantic_functions:riscv32i",
],
)
Cette règle appelle l'outil d'analyse et le générateur ISA pour générer le code C++, puis compile le code généré dans une bibliothèque dont les autres règles peuvent dépendre de l'utilisation de l'étiquette //riscv_isa_decoder:riscv32i_isa
. La section includes
permet de spécifier des fichiers .isa
supplémentaires que le fichier source peut inclure. isa_name
permet de spécifier l'image isa spécifique (requise si plusieurs sont spécifiées) dans le fichier source pour lequel générer le décodeur.
Ajouter des instructions ALU de registre à registre
Il est maintenant temps d'ajouter des instructions au fichier riscv32i.isa
. Le premier groupe d'instructions est constitué d'instructions ALU d'enregistrement et de registre, telles que add
, and
, etc. Sur RiscV32, elles utilisent toutes le format d'instruction binaire de type R:
31..25 | 24..20 | 19..15 | 14..12 | 11..7 | 6..0 |
---|---|---|---|---|---|
7 | 5 | 5 | 3 | 5 | 7 |
func7 | rs2 | rs1 | func3 | rd | code opération |
Bien que le fichier .isa
soit utilisé pour générer un décodeur indépendant du format, il est toujours utile de tenir compte du format binaire et de sa mise en page pour guider les entrées. Comme vous pouvez le voir, trois champs pertinents pour le décodeur alimentent les objets d'instruction: rs2
, rs1
et rd
. À ce stade, nous choisirons d'utiliser ces noms pour les registres d'entiers encodés de la même manière (séquences de bits), dans les mêmes champs d'instructions, dans toutes les instructions.
Les instructions que nous allons ajouter sont les suivantes:
add
: addition d'entiers.and
: AND (ET) au niveau du bit.or
: or au niveau du bit.sll
: décalage logique à gauche.sltu
: défini inférieur à, non signé.sub
: soustraction d'entiers.xor
: fonction xor au niveau du bit.
Chacune de ces instructions sera ajoutée à la section opcodes
de la définition d'emplacement riscv32i
. N'oubliez pas que nous devons spécifier le nom, les opcodes, le désassemblage et la fonction sémantique pour chaque instruction. Le nom est facile, utilisons
simplement les noms OPcode ci-dessus. De plus, comme ils utilisent tous les mêmes opérandes, nous pouvons utiliser { : rs1, rs2 : rd}
pour la spécification de l'opérande. Cela signifie que l'opérande source de registre spécifié par rs1 aura l'indice 0 dans le vecteur d'opérande source de l'objet d'instruction, que l'opérande source de registre spécifié par rs2 aura l'indice 1 et que l'opérande de destination de registre spécifié par rd sera le seul élément du vecteur d'opérande de destination (à l'indice 0).
Vient ensuite la spécification de la fonction sémantique. Pour ce faire, utilisez le mot clé semfunc
et une chaîne C++ qui spécifie un appelable pouvant être utilisé pour attribuer un élément std::function
. Dans ce tutoriel, nous allons utiliser des fonctions. La chaîne appelable sera donc "&MyFunctionName"
. En utilisant le schéma de dénomination suggéré par l'instruction fence
, ils doivent être "&RV32IAdd"
, "&RV32IAnd"
, etc.
Enfin, il y a les
spécifications de démontage. Il commence par le mot clé disasm
, suivi d'une liste de chaînes séparées par une virgule, qui spécifie comment l'instruction doit s'afficher en tant que chaîne. L'utilisation d'un signe %
devant un nom d'opérande indique une substitution de chaîne à l'aide de la représentation de chaîne de cette opérande. Pour l'instruction add
, le format est le suivant: disasm: "add", "%rd,
%rs1,%rs2"
. Cela signifie que l'entrée de l'instruction add
doit se présenter comme suit :
add{ : rs1, rs2 : rd},
semfunc: "&RV32IAdd",
disasm: "add", "%rd, %rs1, %rs2";
Modifiez le fichier riscv32i.isa
et ajoutez toutes ces instructions à la description .isa
. Si vous avez besoin d'aide (ou si vous souhaitez vérifier votre travail), vous pouvez consulter le fichier de description complet sur cette page.
Une fois les instructions ajoutées au fichier riscv32i.isa
, vous devrez ajouter des déclarations de fonction pour chacune des nouvelles fonctions sémantiques référencées dans le fichier rv32i_instructions.h
situé dans "../semantic_functions/". Encore une fois, si vous avez besoin d'aide (ou si vous souhaitez vérifier votre travail), la réponse se trouve sur cette page.
Une fois cette opération terminée, revenez au répertoire riscv_isa_decoder
et effectuez une nouvelle compilation. N'hésitez pas à examiner les fichiers sources générés.
Ajouter des instructions ALU avec des résultats immédiats
L'ensemble d'instructions suivant que nous allons ajouter est constitué d'instructions ALU qui utilisent une valeur immédiate au lieu de l'un des registres. Il existe trois groupes d'instructions (basées sur le champ immédiat) : les instructions immédiates de type I avec une valeur immédiate signée de 12 bits, les instructions immédiates spécialisées de type I pour les décalages et les instructions immédiates de type U, avec une valeur immédiate non signée de 20 bits. Les formats sont indiqués ci-dessous :
Le format I-Type immédiat:
31..20 | 19..15 | 14..12 | 11..7 | 6..0 |
---|---|---|---|---|
12 | 5 | 3 | 5 | 7 |
imm12 | rs1 | func3 | rd | code opération |
Format immédiat spécialisé de type I :
31..25 | 24..20 | 19..15 | 14..12 | 11..7 | 6..0 |
---|---|---|---|---|---|
7 | 5 | 5 | 3 | 5 | 7 |
func7 | uimm5 | rs1 | func3 | rd | code opération |
Format immédiat de type U:
31..12 | 11.7 | 6..0 |
---|---|---|
20 | 5 | 7 |
uimm20 | e | code opération |
Comme vous pouvez le constater, les noms d'opérandes rs1
et rd
font référence aux mêmes champs de bits qu'auparavant et sont utilisés pour représenter des registres entiers. Ces noms peuvent donc être conservés. Les champs de valeur immédiate ont une longueur et un emplacement différents, et deux (uimm5
et uimm20
) sont non signés, tandis que imm12
est signé. Chacun d'entre eux utilisera
son propre nom.
Les opérandes des instructions de type I doivent donc être { : rs1, imm12 :rd
}
. Pour les instructions spécialisées I-Type, il doit s'agir de { : rs1, uimm5 : rd}
.
La spécification de l'opérande d'instruction de type U doit être { : uimm20 : rd }
.
Les instructions I-Type que nous devons ajouter sont les suivantes:
addi
: ajouter immédiatement.andi
: au niveau du bit et avec immédiate.ori
: au niveau du bit ou avec immédiate.xori
: XOR (OU exclusif) au niveau du bit avec valeur immédiate.
Les instructions spécialisées I-Type que nous devons ajouter sont les suivantes:
slli
: décalage logique vers la gauche par immédiat.srai
: décale l'arithmétique vers la droite de manière immédiate.srli
: décale immédiatement la logique vers la droite.
Voici les instructions de type U que nous devons ajouter :
auipc
: ajoute une valeur immédiate supérieure à pc.lui
: charge la partie supérieure immédiatement.
Les noms à utiliser pour les opcodes découlent naturellement des noms d'instructions ci-dessus (pas besoin d'en créer de nouveaux, car ils sont tous uniques). Lorsqu'il s'agit de spécifier des fonctions sémantiques, n'oubliez pas que les objets d'instruction encodent des interfaces vers les opérandes sources qui sont agnostiques par rapport au type d'opérande sous-jacent. Cela signifie que les instructions ayant la même opération, mais pouvant différer en termes de types d'opérandes, peuvent partager la même fonction sémantique. Par exemple, l'instruction addi
effectue la même opération que l'instruction add
si l'une d'elles ignore le type d'opérande. Elle peut donc utiliser la même spécification de fonction sémantique "&RV32IAdd"
. Il en va de même pour andi
, ori
, xori
et slli
.
Les autres instructions utilisent de nouvelles fonctions sémantiques, mais elles doivent être nommées en fonction de l'opération et non des opérandes. Par conséquent, pour srai
, utilisez "&RV32ISra"
. Les instructions U-Type auipc
et lui
n'ont pas d'équivalent dans le registre. Vous pouvez donc utiliser "&RV32IAuipc"
et "&RV32ILui"
.
Les chaînes de désassemblage sont très similaires à celles de l'exercice précédent, mais comme vous pouvez vous y attendre, les références à %rs2
sont remplacées par %imm12
, %uimm5
ou %uimm20
, selon le cas.
Apportez les modifications nécessaires et continuez. Vérifiez la sortie générée. Comme précédemment, vous pouvez comparer votre travail à riscv32i.isa et rv32i_instructions.h.
Ajouter des instructions de branchement et de saut avec lien
Les instructions de branchement et de saut avec lien que nous devons ajouter utilisent toutes les deux un opérande de destination qui n'est implicite que dans l'instruction elle-même, à savoir la valeur de PC suivante. À ce stade, nous allons considérer cela comme un opérande approprié avec le nom next_pc
. Nous y reviendrons dans un prochain tutoriel.
Instructions pour les branches
Les branches que nous ajoutons toutes utilisent l'encodage de type B.
31 | 30..25 | 24..20 | 19..15 | 14..12 | 11..8 | 7 | 6... |
---|---|---|---|---|---|---|---|
1 | 6 | 5 | 5 | 3 | 4 | 1 | 7 |
imm | imm | rs2 | rs1 | func3 | imm | imm | code opération |
Les différents champs immédiats sont concaténés en une valeur immédiate signée de 12 bits. Comme le format n'est pas vraiment pertinent, nous appellerons cette valeur immédiate bimm12
, pour "valeur immédiate de branchement 12 bits". La fragmentation sera abordée dans le prochain tutoriel sur la création du décodeur binaire. Toutes les instructions de branchement comparent les registres d'entiers spécifiés par rs1 et rs2. Si la condition est vraie, la valeur immédiate est ajoutée à la valeur pc actuelle pour générer l'adresse de l'instruction suivante à exécuter. Les opérandes des instructions de branche doivent donc être { : rs1, rs2, bimm12 : next_pc }
.
Les instructions concernant la branche que nous devons ajouter sont les suivantes:
beq
: branche si elle est égale.bge
: branche si la valeur est supérieure ou égale.bgeu
: branchement si la valeur est supérieure ou égale à la valeur non signée.blt
: branche si la valeur est inférieure à.bltu
: branche si la valeur est inférieure à la valeur non signée.bne
: branche si la valeur n'est pas égale.
Ces noms d'opcodes sont tous uniques et peuvent donc être réutilisés dans la description .isa
. Bien entendu, de nouveaux noms de fonctions sémantiques doivent être ajoutés, par exemple :
"&RV32IBeq"
, etc.
La spécification de désassemblage est désormais un peu plus délicate, car l'adresse de l'instruction est utilisée pour calculer la destination, sans qu'elle ne fasse réellement partie des opérandes de l'instruction. Toutefois, il fait partie des informations stockées dans l'objet d'instruction et est donc disponible. La solution consiste à utiliser la syntaxe d'expression dans la chaîne de désassemblage. Au lieu d'utiliser '%' suivi du nom de l'opérande, vous pouvez saisir %(expression: format d'impression). Seules les expressions très simples sont acceptées, mais l'adresse plus le décalage en fait partie, avec le symbole @
utilisé pour l'adresse d'instruction actuelle. Le format d'impression est semblable aux formats printf de style C, mais sans le %
de début. Le format de désassemblage de l'instruction beq
devient alors :
disasm: "beq", "%rs1, %rs2, %(@+bimm12:08x)"
Instructions relatives à l'utilisation de liens
Vous ne devez ajouter que deux instructions de saut et de liaison : jal
(sauter et lien) et jalr
(saut et lien indirect).
L'instruction jal
utilise l'encodage de type J :
31 | 30..21 | 20 | 19..12 | 11.7 | 6... |
---|---|---|---|---|---|
1 | 10 | 1 | 8 | 5 | 7 |
imm | imm | imm | imm | e | code opération |
Tout comme pour les instructions de branchement, l'immédiate de 20 bits est fragmentée sur plusieurs champs. Nous allons donc l'appeler jimm20
. La fragmentation n'est pas importante pour le moment, mais elle sera abordée dans le prochain tutoriel sur la création du décodeur binaire. La spécification de l'opérande devient alors { : jimm20 : next_pc, rd }
. Notez qu'il existe deux opérandes de destination : la valeur pc suivante et le registre de liens spécifié dans l'instruction.
Comme pour les instructions de branchement ci-dessus, le format de désassemblage devient :
disasm: "jal", "%rd, %(@+jimm20:08x)"
Le saut indirect avec lien utilise le format I-Type avec l'immédiat 12 bits. Elle ajoute la valeur immédiate prolongée par signe au registre d'entiers spécifié par rs1
pour produire l'adresse d'instruction cible. Le registre de liaison est le registre d'entier spécifié par rd
.
31..20 | 19..15 | 14..12 | 11..7 | 6..0 |
---|---|---|---|---|
12 | 5 | 3 | 5 | 7 |
imm12 | rs1 | func3 | rd | code opération |
Si vous avez vu ce schéma, vous devez en déduire que la spécification de l'opérande pour jalr
doit être { : rs1, imm12 : next_pc, rd }
, et la spécification de démontage:
disasm: "jalr", "%rd, %rs1, %imm12"
Apportez les modifications, puis effectuez la compilation. Vérifiez le résultat généré. Comme précédemment, vous pouvez vérifier votre travail vis-à-vis de riscv32i.isa et de rv32i_instructions.h.
Ajouter des instructions pour le magasin
Les instructions du magasin sont très simples. Ils utilisent tous le format S-Type :
31..25 | 24..20 | 19..15 | 14..12 | 11..7 | 6..0 |
---|---|---|---|---|---|
7 | 5 | 5 | 3 | 5 | 7 |
imm | rs2 | rs1 | func3 | imm | code opération |
Comme vous pouvez le constater, il s'agit encore d'un cas d'immédiate 12 bits fragmentée, appelons-la simm12
. Les instructions de stockage stockent toutes la valeur du registre d'entier spécifié par rs2 à l'adresse effective en mémoire obtenue en ajoutant la valeur du registre d'entier spécifié par rs1 à la valeur à extension de signe de l'immédiate de 12 bits. L'opérande doit être au format { : rs1, simm12, rs2 }
pour toutes les instructions de magasin.
Les instructions de la plate-forme de téléchargement d'applications à implémenter sont les suivantes :
sb
: octet de stockage.sh
: stocke un demi-mot.sw
: mot de magasin.
La spécification de désassemblage de sb
est la suivante :
disasm: "sb", "%rs2, %simm12(%rs1)"
Les spécifications des fonctions sémantiques sont également celles que vous attendez : "&RV32ISb"
, etc.
Apportez les modifications nécessaires, puis créez votre solution. Vérifiez le résultat généré. Comme précédemment, vous pouvez vérifier votre travail vis-à-vis de riscv32i.isa et de rv32i_instructions.h.
Ajouter des instructions de chargement
Les instructions de chargement sont modélisées un peu différemment des autres instructions dans le simulateur. Afin de pouvoir modéliser les cas où la latence de chargement est incertaine, les instructions de chargement sont divisées en deux actions distinctes: 1) le calcul d'adresse efficace et l'accès à la mémoire, et 2) l'écriture des résultats. Dans le simulateur, cela se fait en divisant l'action sémantique de la charge en deux instructions distinctes, l'instruction principale et une instruction enfant. De plus, lorsque nous spécifions des opérandes, nous devons les spécifier à la fois pour l'instruction principale et pour l'instruction enfant. Pour ce faire, la spécification de l'opérande est traitée comme une liste de triplés. La syntaxe est la suivante:
{(predicate : sources : destinations),
(predicate : sources : destinations), ... }
Les instructions de chargement utilisent toutes le format I-Type, comme beaucoup d'instructions précédentes :
31..20 | 19..15 | 14..12 | 11..7 | 6..0 |
---|---|---|---|---|
12 | 5 | 3 | 5 | 7 |
imm12 | rs1 | func3 | rd | code opération |
La spécification d'opérande divise les opérandes nécessaires pour calculer l'adresse et lancer l'accès à la mémoire à partir de la destination du registre pour les données de chargement : {( : rs1, imm12 : ), ( : : rd) }
.
Étant donné que l'action sémantique est divisée en deux instructions, les fonctions sémantiques doivent également spécifier deux éléments appelables. Pour lw
(mot de chargement), la commande suivante est écrite:
semfunc: "&RV32ILw", "&RV32ILwChild"
La spécification de désassemblage est plus conventionnelle. Aucune mention n'est faite de l'instruction enfant. Pour lw
, il doit s'agir de:
disasm: "lw", "%rd, %imm12(%rs1)"
Voici les instructions de chargement à implémenter:
lb
: octet de chargement.lbu
: octet de chargement non signé.lh
: chargez une demi-mot.lhu
: charge un demi-mot non signé.lw
: chargez le mot.
Apportez les modifications, puis effectuez la compilation. Vérifiez le résultat généré. Comme précédemment, vous pouvez vérifier votre travail vis-à-vis de riscv32i.isa et de rv32i_instructions.h.
Merci d'être arrivé jusqu'ici. Nous espérons avoir répondu à votre question.