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.
- Rédiger les descriptions ISA pour le sous-ensemble d'instructions RiscV RV32I
Présentation
Dans MPACT-Sim, les instructions cibles sont décodées et stockées dans un pour rendre leurs informations plus disponibles, et la sémantique plus rapides à exécuter. Ces instances d'instruction sont mises en cache dans une instruction afin de réduire le nombre de fois où des instructions fréquemment exécutées sont exécuté.
Le cours d'instruction
Avant de commencer, il est utile de regarder
un peu 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 classe Instruction contient toutes les informations nécessaires pour simuler l'instruction lorsqu'elle est « exécutée », par exemple:
- Adresse de l'instruction, taille de l'instruction simulée (par exemple, taille en .text).
- Code d'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 pouvant être appelée.
- Pointeur vers l'objet d'état architectural.
- Pointeur vers l'objet de contexte.
- Pointeur vers les instances d'instruction enfant et suivantes.
- Chaîne de démontage.
Ces instances sont généralement stockées dans un cache d'instructions (instance), et réutilisé chaque fois que l'instruction est réexécutée. Cela permet d'améliorer les performances pendant l'exécution.
À l'exception du pointeur vers l'objet de contexte, tous sont renseignés par la généré à partir de la description ISA. Pour cette il n'est pas nécessaire de connaître le détail de ces éléments, en les utilisant directement. Il est préférable de bien comprendre comment elles sont utilisées. suffisant.
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. Pour
, 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 de
les fonctions sémantiques est abordée 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: le prédicat, la source et la destination. Ces interfaces permettent aux fonctions sémantiques être écrit indépendamment du type réel de l'instruction sous-jacente opérande. Par exemple, l'accès aux valeurs des registres et des 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 ou instances instantanées) peut être implémentées à l'aide de la même fonction sémantique.
L'interface d'opérande du prédicat, pour les ISA qui acceptent le prédicat d'exécution d'instructions (pour les autres ISA, elle est nulle), est utilisée pour 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 valeurs des opérandes d'instructions, sans tenir compte de l'opérande sous-jacent de mots clés. Les méthodes d'interface sont compatibles avec les opérandes scalaires et vectoriels.
// 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
Instances DataBuffer
(type de données interne utilisé pour stocker les valeurs de registre). A
l'opérande de destination est également associé à une latence, qui correspond au nombre
de cycles d'attente jusqu'à ce que l'instance de tampon de données soit allouée par l'instruction
est utilisée pour mettre à jour la valeur du registre cible. Pour
la latence d'une instruction add
peut être de 1, tandis que pour une instruction mpy
,
pour l'instruction, il peut s'agir de 4. Ce point est abordé plus en détail dans le
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. Elle définit l'ensemble les instructions disponibles, les types de données, les registres et d'autres états de machine les instructions, ainsi que leur comportement (sémantique). Aux fins du de MPACT-Sim, l'ISA n'inclut pas l'encodage réel des instructions. Ces données sont traitées séparément.
La norme ISA du processeur est exprimée dans un fichier de description qui décrit d'instructions à un niveau abstrait, sans encodage indépendant. Fichier de description énumère l'ensemble des instructions disponibles. Pour chaque instruction, il est obligatoire d'indiquer son nom, le nombre et les noms de ses opérandes, ainsi que ses à une fonction/appelable C++ qui implémente sa sémantique. En outre, on peut spécifier une chaîne de mise en forme de démontage, et l'utilisation de l'instruction les noms de ressources matérielles. Le premier est utile pour créer un texte représentation de l'instruction de débogage, de traçage ou d'utilisation interactive. La le second permet d'améliorer la précision du cycle 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é en remplissant les champs des objets d'instruction. Les valeurs spécifiques, disons numéro de registre de destination, sont obtenus à partir d’une instruction spécifique à un format décodeur. L'un de ces décodeurs est le décodeur binaire, qui est l'objet principal tutoriel suivant.
Ce tutoriel explique comment écrire un fichier de description ISA pour une requête scalaire simple de l'architecture. Nous allons utiliser un sous-ensemble de l'ensemble d'instructions RiscV RV32I pour Nous illustrerons cela. Avec les autres tutoriels, vous créerez un simulateur capable de simuler une application "Hello World" programme. Pour en savoir plus sur l'ISA RiscV, consultez Caractéristiques Risc-V
Commencez par ouvrir le fichier:
riscv_isa_decoder/riscv32i.isa
Le contenu du fichier est divisé en plusieurs sections. Tout d'abord, l'ISA déclaration:
isa RiscV32I {
namespace mpact::sim::codelab;
slots { riscv32; }
}
Cela déclare RiscV32I
comme le nom de l'ISA et le générateur de code
créez une classe appelée RiscV32IEncodingBase
qui définit l'interface
généré utilise pour obtenir des informations
sur le code d’opération et les opérandes. Le nom de
cette classe est générée en convertissant
le nom ISA en Pascal-case, 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 RiscV32I
ISA (par opposition à une instruction VLIW comportant plusieurs emplacements), et que la seule
Les instructions valides sont celles qui sont définies pour s'exécuter dans riscv32
.
// First disasm fragment is 15 char wide and left justified.
disasm widths = {-15};
Cela indique que le premier fragment de démontage (voir ci-dessous), sera justifié à gauche dans un format de 15 caractères. un champ large. Tous les fragments suivants seront ajoutés à ce champ sans tout espacement supplémentaire.
En dessous, trois déclarations d'emplacement s'affichent: riscv32i
, zicsr
et riscv32
.
D'après la définition isa
ci-dessus, seules les instructions définies pour riscv32
fera partie de RiscV32I
. À quoi servent les deux autres emplacements ?
Les emplacements peuvent être utilisés pour diviser les instructions en groupes distincts, qui peuvent ensuite être
dans un seul emplacement à la fin. Notez la notation : riscv32i, zicsr
dans la déclaration d'emplacement riscv32
. Cela permet de spécifier que l'emplacement riscv32
hérite
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 aux instructions de ces extensions d'être
spécifiées séparément, puis combinées si nécessaire à la fin pour définir
dans son ensemble. Dans ce cas, les instructions du RiscV "I" groupe sont définis
séparément de ceux groupe. 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. comme
nécessaires 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,
Ce tutoriel vise à ajouter les définitions nécessaires à riscv32i
.
emplacement. Examinons de plus près les éléments actuellement définis 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, il y a une section includes {}
qui répertorie les fichiers d'en-tête qui ont besoin
à inclure dans le code généré lorsque cet emplacement est référencé, directement ou
indirectement, dans la ISA finale. Les fichiers inclus peuvent également être répertoriés dans une liste globale
une section includes {}
limitée, auquel cas elles sont toujours incluses. Cela peut
s'avère pratique si le même fichier d'inclusion doit normalement être ajouté à chaque emplacement
définition.
Les déclarations default size
et default latency
définissent que, sauf si
autrement, la taille d'une instruction est de 4, et la latence d'une
d'écriture de l'opérande de destination a zéro cycle. Notez que la taille de l'instruction
spécifiée ici, est la taille de l'incrément de compteur du programme pour calculer le
adresse de l'instruction séquentielle suivante à exécuter dans l'instance
processeur. Elle peut être identique ou non à la taille en octets du fichier
d'instructions 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
les codes d'opérations (instructions) fence
et ebreak
ont été définis jusqu'à présent dans
riscv32i
Le code opérationnel fence
est défini en spécifiant le nom (fence
) et
la spécification de l'opérande ({: imm12 : }
), suivie du démontage (facultatif) ;
("fence"
), et l'appelable qui doit être lié en tant que
("&RV32IFence"
).
Les opérandes d'instruction sont spécifiés sous la forme d'un triple, chaque composant
Séparé par un point-virgule, predicate ':' Liste d'opérandes source ':'
liste d'opérandes de destination. Les listes d'opérandes source et de destination doivent être séparées par une virgule
des listes de noms d'opérandes séparées. Comme vous pouvez le voir, les opérandes d'instruction pour
l'instruction fence
ne contient aucun opérande de prédicat, une seule source ;
nom d'opérande imm12
, et aucun opérande de destination. Le sous-ensemble
RiscV RV32I fait
ne prennent pas en charge l'exécution prédite. L'opérande de prédicat sera donc toujours vide.
dans ce tutoriel.
La fonction sémantique est spécifiée en tant que chaîne nécessaire pour spécifier le code C++
ou appelable à utiliser pour appeler la fonction sémantique. La signature du
fonction sémantique/appelable est void(Instruction *)
.
La spécification de démontage consiste en une liste de chaînes séparées par une virgule.
En règle générale, seules deux chaînes sont utilisées, une pour le code d'opération et une pour le
opérandes. Une fois formaté (à l'aide de l'appel AsString()
dans l'instruction), chaque
chaîne est formatée dans un champ conformément au disasm widths
décrites ci-dessus.
Les exercices suivants vous aident à ajouter des instructions au fichier riscv32i.isa
.
pour simuler une application "Hello World" programme. Pour ceux qui sont pressés,
solutions se trouvent 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. La compilation devrait réussir.
$ cd riscv_isa_decoder
$ bazel build :all
Remettez votre répertoire à la racine du dépôt, puis voyons
aux sources générées. Pour cela, remplacez
le répertoire par
bazel-out/k8-fastbuild/bin/riscv_isa_decoder
(en supposant que vous utilisez un ordinateur x86
host - pour les autres hôtes, k8-fastbuild sera une autre chaîne).
$ cd ..
$ cd bazel-out/k8-fastbuild/bin/riscv_isa_decoder
Ce répertoire contient, entre autres fichiers, les éléments suivants : des fichiers C++ générés:
riscv32i_decoder.h
riscv32i_decoder.cc
riscv32i_enums.h
riscv32i_enums.cc
Cliquez dessus dans le navigateur pour examiner riscv32i_enums.h
. Vous devez
vous verrez qu'elle contient quelque chose comme:
#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éfinis dans la section
riscv32i.isa
est défini dans l'un des types d'énumération. En outre,
il existe un tableau OpcodeNames
qui stocke tous les noms des opcodes (il s'agit
défini dans riscv32i_enums.cc
). Les autres fichiers contiennent le décodeur généré,
que nous aborderons plus en détail dans un autre tutoriel.
Règle de compilation Bazel
Dans Bazel, la cible du décodeur ISA est définie à l'aide d'une macro de règle personnalisée nommée
mpact_isa_decoder
, chargé depuis 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++,
le code généré est ensuite compilé dans une bibliothèque dont d'autres règles peuvent dépendre
l'étiquette //riscv_isa_decoder:riscv32i_isa
. La section includes
est utilisée
pour spécifier des fichiers .isa
supplémentaires que le fichier source peut inclure. La
isa_name
permet de spécifier une autorité de certification spécifique, obligatoire s'il y en a plusieurs
spécifié, dans le fichier source pour lequel générer le décodeur.
Ajouter des instructions ALU d'enregistrement et d'enregistrement
Vous allez maintenant ajouter de nouvelles instructions au fichier riscv32i.isa
. Le premier
L'ensemble d'instructions est constitué d'instructions ALU d'enregistrement et d'enregistrement, telles que add
,
and
, etc. Sous RiscV32, ils utilisent tous le format d'instruction binaire de type R:
31..25 | 24..20 | 19..15 | 14..12 | 11..7 | 6... |
---|---|---|---|---|---|
7 | 5 | 5 | 3 | 5 | 7 |
func7 | rs2 | rs1 | func3 | e | code opération |
Bien que le fichier .isa
soit utilisé pour générer un décodeur indépendant du format, il est
il est utile de tenir compte du format binaire et de sa mise en page pour guider les entrées. En
Vous voyez qu'il y a trois champs pertinents pour le décodeur qui renseigne
objets d'instruction: rs2
, rs1
et rd
. À ce stade, nous choisissons d'utiliser
ces noms pour les registres d'entiers qui sont encodés de la même manière (séquences de bits),
dans les mêmes champs d'instruction, dans toutes les instructions.
Les instructions que nous allons ajouter sont les suivantes:
add
: addition d'un nombre entierand
: opérateur et au niveau du bit.or
: or au niveau du bit.sll
: décalage vers la gauche de la logique.sltu
: défini inférieur à, non signé.sub
: soustraction d'entier.xor
: fonction xor au niveau du bit.
Chacune de ces instructions sera ajoutée à la section opcodes
du
Définition de l'emplacement riscv32i
. Rappelez-vous que nous devons spécifier
le nom, les opcodes,
le démontage et la fonction sémantique de chaque instruction. Le nom est facile,
utilisons simplement les noms
d’opération ci-dessus. De plus, ils utilisent tous
les mêmes opérandes, donc
nous pouvons utiliser { : rs1, rs2 : rd}
pour la spécification de l'opérande. Cela signifie que
l'opérande source du registre spécifié par rs1 aura l'index 0 dans la source
vecteur d'opérande dans l'objet d'instruction, l'opérande d'enregistrement source spécifié
par rs2 auront l'index 1 et l'opérande de destination du registre spécifié par rd
sera le seul élément dans le vecteur d'opérande de destination (à l'index 0).
Vient ensuite la spécification de la fonction sémantique. Pour ce faire, vous pouvez utiliser le mot clé
semfunc
et une chaîne C++ spécifiant un appelable pouvant être utilisé pour attribuer
à un std::function
. Dans ce tutoriel, nous utiliserons des fonctions. Le code pouvant être appelé
la chaîne sera "&MyFunctionName"
. En utilisant le schéma de dénomination suggéré
Instruction fence
. Il doit s'agir de "&RV32IAdd"
, "&RV32IAnd"
, etc.
Enfin, les spécifications de démontage. Elle commence par le mot clé disasm
et
est suivie d'une liste de chaînes séparées par une virgule spécifiant comment
doit s'afficher sous forme de chaîne. Utiliser un signe %
devant un
nom de l'opérande indique une substitution de chaîne à l'aide de la représentation sous forme de chaîne de
cet 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 ressembler
par exemple:
add{ : rs1, rs2 : rd},
semfunc: "&RV32IAdd",
disasm: "add", "%rd, %rs1, %rs2";
Modifiez le fichier riscv32i.isa
et ajoutez toutes ces instructions au
Description de .isa
. Si vous avez besoin d'aide (ou si vous souhaitez vérifier votre travail), l'intégralité
le fichier de description est
cliquez ici.
Une fois les instructions ajoutées au fichier riscv32i.isa
, il sera nécessaire
d'ajouter des déclarations pour chacune des nouvelles fonctions sémantiques
référencée au fichier rv32i_instructions.h
situé dans
'../semantic_functions/. Encore une fois, si vous avez besoin d'aide (ou souhaitez vérifier votre travail),
la réponse est
cliquez ici.
Une fois que c'est fait, continuez et revenez à riscv_isa_decoder
et de le recompiler. N'hésitez pas à examiner les fichiers sources générés.
Ajouter des instructions ALU avec des résultats immédiats
La prochaine série d'instructions que nous ajouterons concerne les instructions ALU qui utilisent un une valeur immédiate au lieu de l'un des registres. Il existe trois groupes de ces instructions (basées sur le champ immédiat): les instructions immédiates I-Type avec une signature immédiate de 12 bits, les instructions spécialisées I-Type immédiates pour les décalages et le type U immédiate, avec une valeur immédiate non signée de 20 bits. Les formats sont présentés ci-dessous:
Le format I-Type immédiat:
31..20 | 19..15 | 14..12 | 11..7 | 6... |
---|---|---|---|---|
12 | 5 | 3 | 5 | 7 |
imm12 | rs1 | func3 | e | code opération |
Le format I-Type spécialisé immédiat:
31..25 | 24..20 | 19..15 | 14..12 | 11..7 | 6... |
---|---|---|---|---|---|
7 | 5 | 5 | 3 | 5 | 7 |
func7 | uimm5 | rs1 | func3 | e | code opération |
Format immédiat de type U:
31..12 | 11..7 | 6... |
---|---|---|
20 | 5 | 7 |
uimm20 | e | code opération |
Comme vous pouvez le voir, les noms d'opérande rs1
et rd
font référence aux mêmes champs de bits que
précédemment, et sont utilisés pour représenter des registres
d'entiers, donc ces noms peuvent être
et conservés. Les champs de valeur immédiate ont une longueur et un emplacement différents.
deux (uimm5
et uimm20
) ne sont pas signées, tandis que imm12
est signée. Chacun de ces éléments
celles-ci utiliseront
leur 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 d'opérande de l'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
: valeur xor au niveau du bit avec immédiat.
Les instructions spécialisées I-Type que nous devons ajouter sont les suivantes:
slli
: décalage immédiat de la logique vers la gauche.srai
: décale l'arithmétique vers la droite de manière immédiate.srli
: décale immédiatement la logique vers la droite.
Les instructions de type U que nous devons ajouter sont les suivantes:
auipc
: ajoute la valeur "upper" immédiate au PC.lui
: charge la partie supérieure immédiatement.
Les noms à utiliser pour les opcodes découlent naturellement des noms des instructions
ci-dessus (inutile d'en créer de nouveaux, ils sont tous uniques). Quand il s'agit de
spécifiant les fonctions sémantiques, rappelez-vous que les objets d'instruction encodent
Des interfaces aux opérandes sources qui sont indépendantes de l'opérande sous-jacent
de mots clés. Cela signifie que pour les instructions
ayant la même opération, mais
peuvent différer en types d'opérandes et partager la même fonction sémantique. Par exemple,
l'instruction addi
effectue la même opération que l'instruction add
si
le type d'opérande est ignoré. Ils peuvent donc utiliser la même fonction
la spécification "&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. Pour srai
, utilisez donc "&RV32ISra"
. La
Les instructions de type U auipc
et lui
n'ont pas d'équivalents dans le registre. Elles sont donc acceptées.
pour utiliser "&RV32IAuipc"
et "&RV32ILui"
.
Les cordes de démontage sont très semblables à celles de l'exercice précédent, mais
comme prévu, 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 le résultat généré. Tout comme auparavant, vous pouvez comparer votre travail riscv32i.isa et rv32i_instructions.h.
Instructions relatives à l'ajout d'une branche et d'un lien Jump-and-Link
Les instructions de branche et de saut et de lien que nous devons ajouter utilisent toutes les deux une destination
opérande qui n'est implicite que dans l'instruction elle-même, à savoir le prochain PC
. À ce stade, nous la traiterons comme un opérande approprié portant le nom
next_pc
Elle sera définie plus en détail dans un tutoriel ultérieur.
Instructions pour l'agence
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 de données immédiates sont concaténés en un mot clé de requête immédiate de 12 bits
. Étant donné que le format n'est pas vraiment pertinent, nous l'appellerons immédiatement
bimm12
, pour une branche 12 bits immédiate. La fragmentation sera traitée
le prochain tutoriel sur la création
du décodeur binaire. Toutes les
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ère l'adresse de l'instruction suivante à exécuter. Les opérandes du
les 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 elle est supérieure ou égale à.bgeu
: branche si supérieure ou égale à non signée.blt
: branche si la valeur est inférieure à.bltu
: branche si la valeur n'est pas déjà signée.bne
: branche si différente.
Ces noms d'opération sont tous uniques et peuvent donc être réutilisés dans le .isa
.
la description. Bien entendu, vous devez ajouter de nouveaux noms de fonctions sémantiques, comme
"&RV32IBeq"
, etc.
La spécification de démontage est désormais un peu plus délicate, puisque l'adresse du
est utilisée pour calculer la destination, sans qu'elle ne fasse partie
des opérandes d'instruction. Cependant, il fait partie des informations
stockées dans
l'objet d'instruction pour qu'il soit disponible. La solution consiste à utiliser
dans la chaîne de désassemblage. Au lieu d'utiliser "%" suivi de
le nom de l'opérande, vous pouvez saisir %(expression: format d'impression). Très simple
les expressions exactes sont acceptées, mais address + offset est l'une d'entre elles, avec @
symbole utilisé pour l'adresse d'instruction actuelle. Le format d'impression est semblable à
formats printf de style C, mais sans %
de début. Le format de démontage de
l'instruction beq
devient alors:
disasm: "beq", "%rs1, %rs2, %(@+bimm12:08x)"
Instructions relatives à l'utilisation de liens
Vous n'avez besoin d'ajouter que deux instructions pour utiliser la fonctionnalité Jump and Link : jal
(jump-and-link) et
jalr
(saut et lien indirects).
L'instruction jal
utilise l'encodage J-Type:
31 | 30..21 | 20 | 19..12 | 11..7 | 6... |
---|---|---|---|---|---|
1 | 10 | 1 | 8 | 5 | 7 |
Imm | Imm | Imm | Imm | e | code opération |
Comme pour les instructions sur la branche, l'immédiate de 20 bits est fragmentée
plusieurs champs. Nous allons donc l'appeler jimm20
. La fragmentation n'est pas importante
pour le moment, mais nous y reviendrons
sur la création du décodeur binaire. 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 la
instruction.
Comme dans les instructions ci-dessus pour les branches, le format de démontage est le suivant:
disasm: "jal", "%rd, %(@+jimm20:08x)"
Le jump-and-link indirect utilise le format I-Type avec le caractère immédiat de 12 bits. Il
ajoute la valeur immédiate étendu par signe au registre d'entiers spécifié par
rs1
pour générer l'adresse d'instruction cible. Le registre de liens est
registre d'entiers spécifié par rd
.
31..20 | 19..15 | 14..12 | 11..7 | 6... |
---|---|---|---|---|
12 | 5 | 3 | 5 | 7 |
imm12 | rs1 | func3 | e | code opération |
Si vous avez vu le motif, vous devez en déduire que la spécification de l'opérande
pour jalr
doit être { : rs1, imm12 : next_pc, rd }
, et l'état de démontage
caractéristiques:
disasm: "jalr", "%rd, %rs1, %imm12"
Apportez les modifications nécessaires, puis créez votre solution. Vérifiez le résultat généré. Juste comme précédemment, vous pouvez comparer votre travail riscv32i.isa et rv32i_instructions.h.
Ajouter des instructions concernant 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... |
---|---|---|---|---|---|
7 | 5 | 5 | 3 | 5 | 7 |
Imm | rs2 | rs1 | func3 | Imm | code opération |
Comme vous pouvez le voir, il s'agit encore d'un autre cas d'imminence fragmentée de 12 bits.
appelez-le simm12
. Les instructions du magasin stockent toutes la valeur de l'entier
de registre spécifié par rs2 à l'adresse effective en mémoire obtenue en ajoutant
la valeur du registre d'entiers spécifié par rs1 à la valeur de signe étendu de
l'immédiat 12 bits. L'opérande doit être au format { : rs1, simm12, rs2 }
pour
toutes les instructions du magasin.
Voici les instructions à implémenter pour le magasin:
sb
: octet de magasin.sh
: stocke un demi-mot.sw
: stocke le mot.
Les spécifications de démontage de sb
sont les suivantes:
disasm: "sb", "%rs2, %simm12(%rs1)"
Les spécifications de la fonction sémantique sont également ce à quoi vous vous attendiez: "&RV32ISb"
,
etc.
Apportez les modifications nécessaires, puis créez votre solution. Vérifiez le résultat généré. Juste comme précédemment, vous pouvez comparer votre travail riscv32i.isa et 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. Pour modéliser les cas où la latence de chargement est incertain, les instructions de chargement sont divisées en deux actions distinctes: 1) effective le calcul d'adresse et l'accès à la mémoire, et 2) l'écriture des résultats. Dans s'effectue en divisant l'action sémantique de la charge en des 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'opérande principal et pour instruction child. Pour ce faire, la spécification de l'opérande est traitée 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 la plupart des instructions:
31..20 | 19..15 | 14..12 | 11..7 | 6... |
---|---|---|---|---|
12 | 5 | 3 | 5 | 7 |
imm12 | rs1 | func3 | e | code opération |
La spécification d'opérande divise les opérandes nécessaires au calcul de l'adresse.
et lancez l'accès à la mémoire à partir de la destination du registre pour les données de chargement:
{( : rs1, imm12 : ), ( : : rd) }
Comme l'action sémantique est divisée en deux instructions, les fonctions sémantiques
de même que deux appelsables. Pour lw
(charger le mot), il s'agit de la formule suivante :
écrit:
semfunc: "&RV32ILw", "&RV32ILwChild"
Les spécifications de démontage sont plus conventionnelles. Aucune mention n'est faite du
pour les enfants. 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
: charge le demi-mot.lhu
: charge un demi-mot non signé.lw
: chargez le mot.
Apportez les modifications nécessaires, puis créez votre solution. Vérifiez le résultat généré. Juste comme précédemment, vous pouvez comparer votre travail riscv32i.isa et rv32i_instructions.h.
Merci d'être arrivé jusqu'ici. Nous espérons que ces informations vous ont été utiles.