Les objectifs de ce tutoriel sont les suivants:
- Découvrez la structure et la syntaxe du fichier de description du format binaire.
- Découvrez comment la description du format binaire correspond à la description de l'ISA.
- Écrivez les descriptions binaires du sous-ensemble d'instructions RiscV RV32I.
Présentation
Encodage des instructions binaires RiscV
L'encodage d'instructions binaires est la méthode standard d'encodage des instructions à exécuter sur un microprocesseur. Elles sont généralement stockées dans un fichier exécutable, généralement au format ELF. Les instructions peuvent avoir une largeur fixe ou variable.
En règle générale, les instructions utilisent un petit ensemble de formats d'encodage, chaque format étant personnalisé en fonction du type d'instructions encodées. Par exemple, les instructions de registre et d'enregistrement peuvent utiliser un format qui maximise le nombre d'opérations disponibles, tandis que les instructions d'enregistrement immédiat en utilisent un autre qui compense le nombre d'opérations disponibles pour augmenter la taille des opérations immédiates pouvant être encodées. Les instructions de branchement et de saut utilisent presque toujours des formats qui maximisent la taille de l'immédiat afin de prendre en charge les branches avec des décalages plus importants.
Les formats d'instructions utilisés par les instructions que nous souhaitons décoder dans notre simulateur RiscV sont les suivants:
Format R, utilisé pour les instructions de registre-registre :
31..25 | 24..20 | 19..15 | 14.12 | 11.7 | 6..0 |
---|---|---|---|---|---|
7 | 5 | 5 | 3 | 5 | 7 |
func7 | rs2 | rs1 | func3 | e | code opération |
Format I-Type, utilisé pour les instructions de registre immédiat, les instructions de chargement et l'instruction jalr
, 12 bits immédiats.
31..20 | 19..15 | 14.12 | 11.7 | 6..0 |
---|---|---|---|---|
12 | 5 | 3 | 5 | 7 |
imm12 | rs1 | func3 | e | code opération |
Format I-Type spécialisé, utilisé pour le décalage avec des instructions immédiates, immédiat à 5 bits :
31..25 | 24..20 | 19..15 | 14.12 | 11.7 | 6..0 |
---|---|---|---|---|---|
7 | 5 | 5 | 3 | 5 | 7 |
func7 | uimm5 | rs1 | func3 | e | code opération |
Format U, utilisé pour les instructions immédiates longues (lui
, auipc
), immédiate à 20 bits :
31..12 | 11.7 | 6..0 |
---|---|---|
20 | 5 | 7 |
uimm20 | rd | code opération |
Format de type B, utilisé pour les branches conditionnelles, immédiat à 12 bits.
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 |
Format J, utilisé pour l'instruction jal
, immédiat 20 bits.
31 | 30..21 | 20 | 19..12 | 11.7 | 6... |
---|---|---|---|---|---|
1 | 10 | 1 | 8 | 5 | 7 |
imm | imm | imm | imm | rd | code opération |
Format S-Type, utilisé pour les instructions de stockage, 12 bits immédiats.
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 voir dans ces formats, toutes ces instructions font 32 bits de long, et les sept bits inférieurs de chaque format correspondent au champ d'opcode. Notez également que même si plusieurs formats ont la même taille immédiate, leurs bits proviennent de différentes parties de l'instruction. Comme nous le verrons, le format de spécification du décodeur binaire peut exprimer cela.
Description de l'encodage binaire
L'encodage binaire de l'instruction est exprimé dans le fichier de description du format binaire (.bin_fmt
). Il décrit l'encodage binaire des instructions dans une ISA afin qu'un décodeur d'instructions au format binaire puisse être généré. Le décodeur généré détermine l'opération, extrait la valeur de l'opérande et les champs immédiats, afin de fournir les informations nécessaires au décodeur indépendant de l'encodage ISA décrit dans le tutoriel précédent.
Dans ce tutoriel, nous allons écrire un fichier de description d'encodage binaire pour un sous-ensemble des instructions RiscV32I nécessaires pour simuler les instructions utilisées dans un petit programme "Hello World". Pour en savoir plus sur la norme ISA RiscV, consultez les spécifications Risc-V{.external}.
Commencez par ouvrir le fichier :
riscv_bin_decoder/riscv32i.bin_fmt
.
Le contenu du fichier est divisé en plusieurs sections.
Tout d'abord, la définition de decoder
.
decoder RiscV32I {
// The namespace in which code will be generated.
namespace mpact::sim::codelab;
// The name (including any namespace qualifiers) of the opcode enum type.
opcode_enum = "OpcodeEnum";
// Include files specific to this decoder.
includes {
#include "riscv_isa_decoder/solution/riscv32i_decoder.h"
}
// Instruction groups for which to generate decode functions.
RiscVInst32;
};
La définition de notre décodeur spécifie le nom de notre décodeur RiscV32I
, ainsi que quatre informations supplémentaires. Le premier est namespace
, qui définit l'espace de noms dans lequel le code généré sera placé. Deuxièmement, le opcode_enum
, qui indique comment le type d'énumération d'opcode généré par le décodeur ISA doit être référencé dans le code généré. Troisièmement, includes {}
spécifie les fichiers d'inclusion nécessaires au code généré pour ce décodeur.
Dans notre cas, il s'agit du fichier produit par le décodeur ISA du tutoriel précédent.
Des fichiers d'inclusion supplémentaires peuvent être spécifiés dans une définition includes {}
à portée globale. Cela est utile si plusieurs décodeurs sont définis et qu'ils doivent tous inclure certains des mêmes fichiers. La quatrième partie est une liste de noms de groupes d'instructions qui constituent les instructions pour lesquelles le décodeur est généré. Dans notre cas, il n'y en a qu'un seul : RiscVInst32
.
Ensuite, il y a trois définitions de format. Ils représentent différents formats d'instructions pour un mot d'instruction 32 bits utilisé par les instructions déjà définies dans le fichier.
// The generic RiscV 32 bit instruction format.
format Inst32Format[32] {
fields:
unsigned bits[25];
unsigned opcode[7];
};
// RiscV 32 bit instruction format used by a number of instructions
// needing a 12 bit immediate, including CSR instructions.
format IType[32] : Inst32Format {
fields:
signed imm12[12];
unsigned rs1[5];
unsigned func3[3];
unsigned rd[5];
unsigned opcode[7];
};
// RiscV instruction format used by fence instructions.
format Fence[32] : Inst32Format {
fields:
unsigned fm[4];
unsigned pred[4];
unsigned succ[4];
unsigned rs1[5];
unsigned func3[3];
unsigned rd[5];
unsigned opcode[7];
};
Le premier définit un format d'instruction de 32 bits nommé Inst32Format
qui comporte deux champs : bits
(25 bits) et opcode
(7 bits). Chaque champ est unsigned
, ce qui signifie que la valeur sera étendue à zéro lorsqu'elle sera extraite et placée dans un type d'entier C++. La somme des largeurs des champs de bits doit être égale à la largeur du format. L'outil génère une erreur en cas de désaccord. Ce format ne dérive d'aucun autre format. Il est donc considéré comme un format de niveau supérieur.
Le second définit un format d'instruction de 32 bits nommé IType
qui est dérivé de Inst32Format
, ce qui rend ces deux formats liés. Le format contient cinq champs : imm12
, rs1
, func3
, rd
et opcode
. Le champ imm12
est signed
, ce qui signifie que la valeur sera étendue avec signe lorsqu'elle sera extraite et placée dans un type d'entier C++. Notez que IType.opcode
possède le même attribut signé/non signé et fait référence aux mêmes bits de mot d'instruction que Inst32Format.opcode
.
Le troisième format est un format personnalisé qui n'est utilisé que par l'instruction fence
, qui est une instruction déjà spécifiée et dont nous n'avons pas à nous soucier dans ce tutoriel.
Point clé: Réutilisez les noms de champs dans différents formats liés à condition qu'ils représentent les mêmes bits et qu'ils aient le même attribut signé/non signé.
Après les définitions de format dans riscv32i.bin_fmt
, vient une définition de groupe d'instructions. Toutes les instructions d'un groupe d'instructions doivent avoir la même longueur de bits et utiliser un format qui dérive (peut-être indirectement) du même format d'instruction de niveau supérieur. Lorsqu'une ISA peut comporter des instructions de différentes longueurs, un groupe d'instructions différent est utilisé pour chaque longueur. De plus, si le décodage de l'ISA cible dépend d'un mode d'exécution, comme les instructions Arm et Thumb, un groupe d'instructions distinct est requis pour chaque mode. L'analyseur bin_fmt
génère un décodeur binaire pour chaque groupe d'instructions.
instruction group RiscV32I[32] "OpcodeEnum" : Inst32Format {
fence : Fence : func3 == 0b000, opcode == 0b000'1111;
csrs : IType : func3 == 0b010, rs1 != 0, opcode == 0b111'0011;
csrw_nr : IType : func3 == 0b001, rd == 0, opcode == 0b111'0011;
csrs_nw : IType : func3 == 0b010, rs1 == 0, opcode == 0b111'0011;
};
Le groupe d'instructions définit un nom RiscV32I
, une largeur [32]
, le nom du type d'énumération de code d'opération à utiliser "OpcodeEnum"
et un format d'instruction de base. Le type d'énumération OPcode doit être identique à celui produit par le décodeur d'instructions indépendant du format abordé dans le tutoriel sur le décodeur ISA.
La description de l'encodage de chaque instruction se compose de trois parties:
- Nom de l'opcode, qui doit être identique à celui utilisé dans la description du décodeur d'instructions pour que les deux fonctionnent ensemble.
- Format d'instruction à utiliser pour l'opcode. Il s'agit du format utilisé pour satisfaire les références aux champs de bits dans la partie finale.
- Liste de contraintes de champ de bits,
==
,!=
,<
,<=
,>
et>=
, séparés par une virgule, qui doivent tous être vrais pour que l'opcode corresponde au mot d'instruction.
L'analyseur .bin_fmt
utilise toutes ces informations pour créer un décodeur qui :
- Fournit les fonctions d'extraction (signées/non signées) appropriées pour chaque champ de bits dans tous les formats. Les fonctions d'extracteur sont placées dans des espaces de noms nommés par la version snake-case du nom de format. Par exemple, les fonctions d'extraction pour le format
IType
sont placées dans l'espace de nomsi_type
. Chaque fonction d'extraction est déclaréeinline
, prend le typeuint_t
le plus étroit qui contient la largeur du format et renvoie le typeint_t
(pour les valeurs signées) ouuint_t
(pour les valeurs non signées) le plus étroit, qui contient la largeur du champ extrait. Par exemple :
inline uint8_t ExtractOpcode(uint32_t value) {
return value & 0x7f;
}
- Une fonction de décodage pour chaque groupe d'instructions. Il renvoie une valeur de type
OpcodeEnum
et utilise le typeuint_t
le plus étroit qui contient la largeur du format de groupe d'instructions.
Effectuer la compilation initiale
Accédez au répertoire riscv_bin_decoder
et créez le projet à l'aide de la commande suivante :
$ cd riscv_bin_decoder
$ bazel build :all
Revenez maintenant au répertoire racine du dépôt, puis examinons les sources générées. Pour ce faire, modifiez le répertoire en bazel-out/k8-fastbuild/bin/riscv_bin_decoder
(en supposant que vous vous trouvez 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_bin_decoder
riscv32i_bin_decoder.h
riscv32i_bin_decoder.cc
Fichier d'en-tête généré (.h)
Ouvrez riscv32i_bin_decoder.h
. La première partie du fichier contient des protections standards, des fichiers d'inclusion et des déclarations de namespace. Ensuite, il existe une fonction d'assistance modélisée dans l'espace de noms internal
. Cette fonction permet d'extraire les champs de bits de formats trop longs pour tenir dans un entier C++ de 64 bits.
#ifndef RISCV32I_BIN_DECODER_H
#define RISCV32I_BIN_DECODER_H
#include <iostream>
#include <cstdint>
#include "third_party/absl/functional/any_invocable.h"
#include "learning/brain/research/mpact/sim/codelab/riscv_isa_decoder/solution/riscv32i_decoder.h"
namespace mpact {
namespace sim {
namespace codelab {
namespace internal {
template <typename T>
static inline T ExtractBits(const uint8_t *data, int data_size,
int bit_index, int width) {
if (width == 0) return 0;
int byte_pos = bit_index >> 3;
int end_byte = (bit_index + width - 1) >> 3;
int start_bit = bit_index & 0x7;
// If it is only from one byte, extract and return.
if (byte_pos == end_byte) {
uint8_t mask = 0xff >> start_bit;
return (mask & data[byte_pos]) >> (8 - start_bit - width);
}
// Extract from the first byte.
T val = 0;
val = data[byte_pos++] & 0xff >> start_bit;
int remainder = width - (8 - start_bit);
while (remainder >= 8) {
val = (val << 8) | data[byte_pos++];
remainder -= 8;
}
// Extract any remaining bits.
if (remainder > 0) {
val <<= remainder;
int shift = 8 - remainder;
uint8_t mask = 0b1111'1111 << shift;
val |= (data[byte_pos] & mask) >> shift;
}
return val;
}
} // namespace internal
Après la section initiale, il y a un ensemble de trois espaces de noms, un pour chacune des déclarations format
du fichier riscv32i.bin_fmt
:
namespace fence {
...
} // namespace fence
namespace i_type {
...
} // namespace i_type
namespace inst32_format {
...
} // namespace inst32_format
Dans chacun de ces espaces de noms, la fonction d'extraction de champ de bits inline
pour chaque champ de bits de ce format est définie. En outre, le format de base duplique les fonctions d'extraction des formats descendants pour lesquels 1) les noms de champs ne figurent que dans un seul nom de champ ou 2) pour lesquels les noms de champ font référence au même type de champ (signé/non signé et positions de bits) dans chaque format dans lequel ils apparaissent. Cela permet d'extraire les champs de bits qui décrivent les mêmes bits à l'aide de fonctions dans l'espace de noms de format de niveau supérieur.
Les fonctions de l'espace de noms i_type
sont affichées ci-dessous :
namespace i_type {
inline uint8_t ExtractFunc3(uint32_t value) {
return (value >> 12) & 0x7;
}
inline int16_t ExtractImm12(uint32_t value) {
int16_t result = ( (value >> 20) & 0xfff) << 4;
result = result >> 4;
return result;
}
inline uint8_t ExtractOpcode(uint32_t value) {
return value & 0x7f;
}
inline uint8_t ExtractRd(uint32_t value) {
return (value >> 7) & 0x1f;
}
inline uint8_t ExtractRs1(uint32_t value) {
return (value >> 15) & 0x1f;
}
} // namespace i_type
Enfin, la déclaration de fonction du décodeur pour le groupe d'instructions RiscVInst32
est déclarée. Il prend un entier non signé de 32 bits comme valeur du mot d'instruction et renvoie le membre de la classe d'énumération OpcodeEnum
correspondant, ou OpcodeEnum::kNone
en cas de non-correspondance.
OpcodeEnum DecodeRiscVInst32(uint32_t inst_word);
Le fichier source (.cc) généré
Ouvrez maintenant riscv32i_bin_decoder.cc
. La première partie du fichier contient les déclarations #include
et d'espace de noms, suivies des déclarations de fonction de décodeur :
#include "riscv32i_bin_decoder.h"
namespace mpact {
namespace sim {
namespace codelab {
OpcodeEnum DecodeRiscVInst32None(uint32_t);
OpcodeEnum DecodeRiscVInst32_0(uint32_t inst_word);
OpcodeEnum DecodeRiscVInst32_0_3(uint32_t inst_word);
OpcodeEnum DecodeRiscVInst32_0_3c(uint32_t inst_word);
OpcodeEnum DecodeRiscVInst32_0_5c(uint32_t inst_word);
DecodeRiscVInst32None
est utilisé pour les actions de décodage vides, c'est-à-dire celles qui renvoient OpcodeEnum::kNone
. Les trois autres fonctions constituent le décodeur généré. Le décodeur global fonctionne de manière hiérarchique. Un ensemble de bits dans le mot d'instruction est calculé pour différencier les instructions ou les groupes d'instructions au niveau supérieur. Les bits ne doivent pas être contigus. Le nombre de bits détermine la taille d'une table de recherche qui est renseignée avec des fonctions de décodeur de deuxième niveau. Cela apparaît dans la section suivante du fichier :
absl::AnyInvocable<OpcodeEnum(uint32_t)> parse_group_RiscVInst32_0[kParseGroupRiscVInst32_0_Size] = {
&DecodeRiscVInst32None, &DecodeRiscVInst32None,
&DecodeRiscVInst32None, &DecodeRiscVInst32_0_3,
&DecodeRiscVInst32None, &DecodeRiscVInst32None,
...
&DecodeRiscVInst32None, &DecodeRiscVInst32None,
&DecodeRiscVInst32None, &DecodeRiscVInst32None,
&DecodeRiscVInst32_0_3c, &DecodeRiscVInst32None,
...
};
Enfin, les fonctions de décodeur sont définies :
OpcodeEnum DecodeRiscVInst32None(uint32_t) {
return OpcodeEnum::kNone;
}
OpcodeEnum DecodeRiscVInst32_0(uint32_t inst_word) {
if ((inst_word & 0x4003) != 0x3) return OpcodeEnum::kNone;
uint32_t index;
index = (inst_word >> 2) & 0x1f;
index |= (inst_word >> 7) & 0x60;
return parse_group_RiscVInst32_0[index](inst_word);
}
OpcodeEnum DecodeRiscVInst32_0_3(uint32_t inst_word) {
return OpcodeEnum::kFence;
}
OpcodeEnum DecodeRiscVInst32_0_3c(uint32_t inst_word) {
if ((inst_word & 0xf80) != 0x0) return OpcodeEnum::kNone;
return OpcodeEnum::kCsrwNr;
}
OpcodeEnum DecodeRiscVInst32_0_5c(uint32_t inst_word) {
uint32_t rs1_value = (inst_word >> 15) & 0x1f;
if (rs1_value != 0x0)
return OpcodeEnum::kCsrs;
if (rs1_value == 0x0)
return OpcodeEnum::kCsrsNw;
return OpcodeEnum::kNone;
}
OpcodeEnum DecodeRiscVInst32(uint32_t inst_word) {
OpcodeEnum opcode;
opcode = DecodeRiscVInst32_0(inst_word);
return opcode;
}
Dans ce cas, où seulement quatre instructions sont définies, il n'y a qu'un seul niveau de décodage et une table de conversion très creuse. À mesure que des instructions sont ajoutées, la structure du décodeur change et le nombre de niveaux dans la hiérarchie de la table du décodeur peut augmenter.
Ajouter des instructions ALU d'enregistrement et d'enregistrement
Il est maintenant temps d'ajouter des instructions au fichier riscv32i.bin_fmt
. Le premier groupe d'instructions est constitué d'instructions ALU registre-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 | e | code opération |
La première chose que nous devons faire est d'ajouter le format. Ouvrez riscv32i.bin_fmt
dans votre éditeur préféré. Juste après Inst32Format
, vous pouvez ajouter un format appelé RType
, qui dérive de Inst32Format
. Tous les champs de bits de RType
sont unsigned
. Utilisez les noms, la largeur de bits et l'ordre (de gauche à droite) du tableau ci-dessus pour définir le format. Si vous avez besoin d'un indice ou si vous souhaitez voir la solution complète, cliquez ici.
Nous devons ensuite ajouter les instructions. Voici les instructions à suivre :
add
: addition d'un nombre entierand
: AND (ET) au niveau du bit.or
: ou 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.
Voici leurs encodages:
31..25 | 24..20 | 19..15 | 14.12 | 11.7 | 6..0 | nom de l'instruction |
---|---|---|---|---|---|---|
000 000 | rs2 | rs1 | 000 | rd | 011 0011 | add |
000 0000 | rs2 | rs1 | 111 | e | 011 0011 | et |
000 0000 | rs2 | rs1 | 110 | e | 011 0011 | ou |
000 0000 | rs2 | rs1 | 001 | e | 011 0011 | sll |
000 0000 | rs2 | rs1 | 011 | rd | 011 0011 | sltu |
010 0000 | rs2 | rs1 | 000 | rd | 011 0011 | Pub/Sub. |
000 0000 | rs2 | rs1 | 100 | rd | 011 0011 | xor |
func7 | func3 | code opération |
Ajoutez ces définitions d'instructions avant les autres instructions du groupe d'instructions RiscVInst32
. Les chaînes binaires sont spécifiées avec un préfixe 0b
(similaire à 0x
pour les nombres hexadécimaux). Pour faciliter la lecture de longues chaînes de chiffres binaires, vous pouvez également insérer le guillemet simple '
comme séparateur de chiffres là où vous le souhaitez.
Chacune de ces définitions d'instructions comporte trois contraintes, à savoir sur func7
, func3
et opcode
. Pour tous les éléments sauf sub
, la contrainte func7
sera la suivante :
func7 == 0b000'0000
La contrainte func3
varie dans la plupart des instructions. Pour add
et sub
, il s'agit de:
func3 == 0b000
La contrainte opcode
est la même pour chacune des instructions suivantes:
opcode == 0b011'0011
N'oubliez pas de terminer chaque ligne par un point-virgule ;
.
La solution finale est disponible sur cette page.
Créez maintenant votre projet comme précédemment, puis ouvrez le fichier riscv32i_bin_decoder.cc
généré. Vous constaterez que des fonctions de décodeur supplémentaires ont été générées pour gérer les nouvelles instructions. Pour la plupart, ils sont similaires à ceux qui ont été générés précédemment, mais examinez DecodeRiscVInst32_0_c
, qui est utilisé pour le décodage add
/sub
:
OpcodeEnum DecodeRiscVInst32_0_c(uint32_t inst_word) {
static constexpr OpcodeEnum opcodes[2] = {
OpcodeEnum::kAdd,
OpcodeEnum::kSub,
};
if ((inst_word & 0xbe000000) != 0x0) return OpcodeEnum::kNone;
uint32_t index;
index = (inst_word >> 30) & 0x1;
return opcodes[index];
}
Dans cette fonction, une table de décodage statique est générée, et une valeur de recherche est extraite du mot d'instruction pour sélectionner l'index approprié. Cela ajoute une deuxième couche dans la hiérarchie du décodeur d'instructions, mais comme l'opcode peut être recherché directement dans un tableau sans autres comparaisons, il est intégré dans cette fonction au lieu d'exiger un autre appel de fonction.
Ajouter des instructions ALU avec des 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 :
Format immédiat de type I :
31..20 | 19..15 | 14.12 | 11.7 | 6..0 |
---|---|---|---|---|
12 | 5 | 3 | 5 | 7 |
imm12 | rs1 | func3 | e | 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 | e | code opération |
Format immédiat de type U:
31..12 | 11.7 | 6..0 |
---|---|---|
20 | 5 | 7 |
uimm20 | rd | code opération |
Le format I-Type existe déjà dans riscv32i.bin_fmt
. Il n'est donc pas nécessaire d'ajouter ce format.
Si nous comparons le format I-Type spécialisé au format R-Type que nous avons défini dans l'exercice précédent, nous constatons que la seule différence est que les champs rs2
sont renommés uimm5
. Au lieu d'ajouter un tout nouveau format, nous pouvons
augmenter le format R-Type. Nous ne pouvons pas ajouter un autre champ, car cela augmenterait la largeur du format, mais nous pouvons ajouter une superposition. Une superposition est un alias d'un ensemble de bits dans le format et peut être utilisée pour combiner plusieurs sous-séquences du format dans une entité nommée distincte. L'inconvénient est que le code généré inclura désormais également une fonction d'extraction pour la superposition, en plus de celles pour les champs. Dans ce cas, lorsque rs2
et uimm5
sont non signés, cela ne fait pas beaucoup de différence, sauf pour indiquer explicitement que le champ est utilisé comme immédiat. Pour ajouter une superposition nommée uimm5
au format R-Type, ajoutez ce qui suit après le dernier champ :
overlays:
unsigned uimm5[5] = rs2;
Le seul nouveau format que nous devons ajouter est le format U-Type. Avant d'ajouter le format, examinons les deux instructions qui l'utilisent : auipc
et lui
. Ces deux éléments déplacent la valeur immédiate de 20 bits de 12 avant de l'utiliser pour y ajouter le PC (auipc
) ou l'écrire directement dans un registre (lui
). En utilisant une superposition, nous pouvons fournir une version pré-décalée de l'immédiat, en décalant une petite partie du calcul de l'exécution de l'instruction au décodage d'instructions. Ajoutez d'abord le format en fonction des champs spécifiés dans le tableau ci-dessus. Nous pouvons ensuite ajouter la superposition suivante:
overlays:
unsigned uimm32[32] = uimm20, 0b0000'0000'0000;
La syntaxe de superposition nous permet de concaténer non seulement des champs, mais aussi des littéraux. Dans ce cas, nous le concaténons avec 12 zéros, ce qui le décale de 12 positions vers la gauche.
Les instructions I-Type que nous devons ajouter sont les suivantes:
addi
: ajoutez immédiatement.andi
: AND (ET) bit à bit avec valeur immédiate.ori
: OR (OU) au niveau du bit avec valeur immédiate.xori
: XOR (OU exclusif) au niveau du bit avec valeur immédiate.
Voici leurs encodages:
31..20 | 19..15 | 14.12 | 11.7 | 6..0 | opcode_name |
---|---|---|---|---|---|
imm12 | rs1 | 000 | e | 001 0011 | addi |
imm12 | rs1 | 111 | rd | 001 0011 | andi |
imm12 | rs1 | 110 | rd | 001 0011 | ori |
imm12 | rs1 | 100 | rd | 001 0011 | xori |
func3 | code opération |
Les instructions de type R (type I spécialisé) que nous devons ajouter sont les suivantes :
slli
: décalage logique vers la gauche par immédiat.srai
: décalage arithmétique vers la droite par valeur immédiate.srli
: décalage logique à droite par valeur immédiate.
Leurs encodages sont les suivants :
31..25 | 24..20 | 19..15 | 14.12 | 11.7 | 6..0 | nom de l'instruction |
---|---|---|---|---|---|---|
000 0000 | uimm5 | rs1 | 001 | rd | 001 0011 | slli |
010 0000 | uimm5 | rs1 | 101 | rd | 001 0011 | srai |
000 000 | uimm5 | rs1 | 101 | rd | 001 0011 | srli |
func7 | func3 | code opération |
Voici les instructions de type U que nous devons ajouter :
auipc
: ajoute une valeur immédiate supérieure à pc.lui
: chargement immédiat supérieur.
Leurs encodages sont les suivants :
31..12 | 11.7 | 6..0 | nom de l'instruction |
---|---|---|---|
uimm20 | rd | 001 0111 | auipc |
uimm20 | rd | 011 0111 | lui |
code opération |
Apportez les modifications nécessaires, puis créez votre solution. Vérifiez la sortie générée. Comme précédemment, vous pouvez comparer votre travail à riscv32i.bin_fmt.
Ajouter des instructions de branchement et de saut avec lien
L'ensemble d'instructions suivant qui doit être défini est constitué des instructions de branchement conditionnel, de l'instruction de saut et de lien et de l'instruction de registre de saut et de lien.
Les branches conditionnelles que nous ajoutons toutes utilisent l'encodage de type B.
31..25 | 24..20 | 19..15 | 14.12 | 11.7 | 6..0 |
---|---|---|---|---|---|
7 | 5 | 5 | 3 | 5 | 7 |
imm7 | rs2 | rs1 | func3 | imm5 | code opération |
Bien que la mise en page de l'encodage de type B soit identique à celle de l'encodage de type R, nous avons choisi d'utiliser un nouveau type de format pour qu'il soit conforme à la documentation RiscV.
Vous auriez également pu ajouter une superposition pour obtenir immédiatement le décalage de branche approprié, à l'aide des champs func7
et rd
de l'encodage R-Type.
Ajouter un format BType
avec les champs spécifiés ci-dessus est nécessaire, mais pas suffisant. Comme vous pouvez le voir, le résultat immédiat est divisé en deux champs d'instruction.
De plus, les instructions de branche ne traitent pas cela comme une simple concaténation des deux champs. Au lieu de cela, chaque champ est partitionné davantage, et ces partitions sont concaténées dans un ordre différent. Enfin, cette valeur est décalée vers la gauche d'un pour obtenir un décalage aligné sur 16 bits.
La séquence de bits du mot d'instruction utilisée pour former l'immédiat est la suivante : 31, 7, 30..25, 11..8. Cela correspond aux références de sous-champs suivantes, où l'index ou la plage spécifient les bits du champ, numérotés de droite à gauche, c'est-à-dire :
imm7[6]
fait référence au msb de imm7
, et imm5[0]
fait référence au lsb de imm5
.
imm7[6], imm5[0], imm7[5..0], imm5[4..1]
Faire de cette manipulation de bits une partie des instructions de branchement présente deux grands inconvénients. Tout d'abord, elle lie l'implémentation de la fonction sémantique aux détails de la représentation de l'instruction binaire. Deuxièmement, cela ajoute des frais généraux d'exécution. La réponse consiste à ajouter une superposition au format BType
, y compris un "0" à la fin pour tenir compte du décalage vers la gauche.
overlays:
signed b_imm[13] = imm7[6], imm5[0], imm7[5..0], imm5[4..1], 0b0;
Notez que la superposition est signée. Elle sera donc automatiquement étendue avec signe lorsqu'elle sera extraite du mot d'instruction.
L'instruction de saut et de liaison (immédiate) utilise l'encodage de type J :
31..12 | 11.7 | 6..0 |
---|---|---|
20 | 5 | 7 |
imm20 | rd | code opération |
Il s'agit également d'un format facile à ajouter, mais là encore, l'immédiat utilisé par l'instruction n'est pas aussi simple qu'il n'y paraît. Les séquences de bits utilisées pour former l'immédiate complète sont les suivantes : 31, 19..12, 20, 30..21, et l'immédiate finale est décalée vers la gauche d'un pour l'alignement sur demi-mot. La solution consiste à ajouter une autre superposition (21 bits pour tenir compte du décalage à gauche) au format :
overlays:
signed j_imm[21] = imm20[19, 7..0, 8, 18..9], 0b0;
Comme vous pouvez le voir, la syntaxe des superpositions accepte la spécification de plusieurs plages dans un champ dans un format raccourci. De plus, si aucun nom de champ n'est utilisé, les numéros de bits font référence au mot d'instruction lui-même. L'exemple ci-dessus peut donc être écrit comme suit :
signed j_imm[21] = [31, 19..12, 20, 30..21], 0b0;
Enfin, le saut et lien (registre) utilise le format de type I comme précédemment.
Format immédiat de type I :
31..20 | 19..15 | 14.12 | 11.7 | 6..0 |
---|---|---|---|---|
12 | 5 | 3 | 5 | 7 |
imm12 | rs1 | func3 | e | code opération |
Cette fois, aucune modification n'est requise au niveau du format.
Les instructions concernant la branche que nous devons ajouter sont les suivantes:
beq
: branche si égal.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.
Ils sont encodés comme suit:
31..25 | 24..20 | 19..15 | 14.12 | 11.7 | 6..0 | nom de l'instruction |
---|---|---|---|---|---|---|
imm7 | rs2 | rs1 | 000 | imm5 | 110 0011 | beq |
imm7 | rs2 | rs1 | 101 | imm5 | 110 0011 | bge |
imm7 | rs2 | rs1 | 111 | imm5 | 110 0011 | bgeu |
imm7 | rs2 | rs1 | 100 | imm5 | 110 0011 | blt |
imm7 | rs2 | rs1 | 110 | imm5 | 110 0011 | bltu |
imm7 | rs2 | rs1 | 001 | imm5 | 110 0011 | bne |
func3 | code opération |
L'instruction jal
est encodée comme suit:
31..12 | 11.7 | 6..0 | nom de l'opération |
---|---|---|---|
imm20 | rd | 110 1111 | jal |
code opération |
L'instruction jalr
est encodée comme suit :
31..20 | 19..15 | 14.12 | 11.7 | 6..0 | opcode_name |
---|---|---|---|---|---|
imm12 | rs1 | 000 | rd | 110 0111 | jalr |
func3 | code opération |
Apportez les modifications, puis effectuez la compilation. Vérifiez le résultat généré. Comme précédemment, vous pouvez comparer votre travail à riscv32i.bin_fmt.
Ajouter des instructions pour le magasin
Les instructions de stockage utilisent l'encodage de type S, qui est identique à l'encodage de type B utilisé par les instructions de branchement, à l'exception de la composition de l'immédiat. Nous avons choisi d'ajouter le format SType
pour rester en phase avec la documentation RiscV.
31..25 | 24..20 | 19..15 | 14.12 | 11.7 | 6..0 |
---|---|---|---|---|---|
7 | 5 | 5 | 3 | 5 | 7 |
imm7 | rs2 | rs1 | func3 | imm5 | code opération |
Dans le cas du format SType
, l'immédiat est heureusement une concaténation simple des deux champs immédiats. La spécification de superposition est donc simplement la suivante :
overlays:
signed s_imm[12] = imm7, imm5;
Notez qu'aucun spécificateur de plage de bits n'est requis lors de la concatenaison de champs entiers.
Les instructions de magasin sont encodées comme suit :
31..25 | 24..20 | 19..15 | 14.12 | 11.7 | 6..0 | nom de l'instruction |
---|---|---|---|---|---|---|
imm7 | rs2 | rs1 | 000 | imm5 | 010 0011 | sb |
imm7 | rs2 | rs1 | 001 | imm5 | 010 0011 | sh |
imm7 | rs2 | rs1 | 010 | imm5 | 010 0011 | sw |
func3 | code opération |
Apportez les modifications, puis effectuez la compilation. Vérifiez la sortie générée. Comme précédemment, vous pouvez comparer votre travail à riscv32i.bin_fmt.
Ajouter des instructions de chargement
Les instructions de chargement utilisent le format I-Type. Aucune modification n'est nécessaire.
Les encodages sont les suivants :
31..20 | 19..15 | 14.12 | 11.7 | 6..0 | opcode_name |
---|---|---|---|---|---|
imm12 | rs1 | 000 | e | 000 0011 | lb |
imm12 | rs1 | 100 | rd | 000 0011 | lbu |
imm12 | rs1 | 001 | e | 000 0011 | lh |
imm12 | rs1 | 101 | rd | 000 0011 | lhu |
imm12 | rs1 | 010 | rd | 000 0011 | lw |
func3 | code opération |
Apportez les modifications nécessaires, puis créez votre solution. Vérifiez la sortie générée. Comme précédemment, vous pouvez comparer votre travail à riscv32i.bin_fmt.
Ce tutoriel est maintenant terminé. Nous espérons qu'il vous a été utile.