Tutoriel sur le décodeur d'instructions binaires

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 noms i_type. Chaque fonction d'extraction est déclarée inline, prend le type uint_t le plus étroit qui contient la largeur du format et renvoie le type int_t (pour les valeurs signées) ou uint_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 type uint_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 entier
  • and : 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.


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.