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 au format binaire.
  • Découvrez la correspondance entre la description du format binaire et la description de l'ISA.
  • Rédigez les descriptions binaires du sous-ensemble d'instructions RiscV RV32I.

Présentation

Encodage d'instructions binaires RiscV

L'encodage d'instructions binaires est la méthode standard pour encoder les instructions pour sur un microprocesseur. Ils sont normalement stockés dans un fichier exécutable, généralement au format ELF. Les instructions peuvent avoir une largeur fixe ou être variables la largeur.

En règle générale, les instructions utilisent un petit ensemble de formats d'encodage, chacun d'entre eux personnalisés pour le type d'instructions encodées. Par exemple, "register-register" les instructions peuvent utiliser un format qui maximise le nombre d'opérations disponibles, tandis que les instructions d'enregistrement immédiat en utilisent une autre qui compromet le nombre de disponibles pour augmenter la taille de l’élément direct qui peut être encodé. Les instructions Branch et jump utilisent presque toujours des formats qui maximisent la taille afin de prendre en charge les branches avec des décalages plus importants.

Les formats d'instructions utilisés par les instructions que nous voulons décoder dans notre code RiscV sont les suivants:

Format R-Type utilisé pour les instructions d'enregistrement et d'enregistrement:

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

format I-Type, utilisé pour les instructions d’enregistrement immédiat, les instructions de chargement et l'instruction jalr, 12 bits immédiat.

31..20 19..15 14..12 11..7 6...
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, 5 bits 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 U-Type, utilisé pour les instructions immédiates longues (lui, auipc), 20 bits immédiat:

31..12 11..7 6...
20 5 7
uimm20 e code opération

Format B-Type, utilisé pour les branches conditionnelles, 12 bits immédiat.

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-Type, utilisé pour l'instruction jal, immédiatement sur 20 bits.

31 30..21 20 19..12 11..7 6...
1 10 1 8 5 7
Imm Imm Imm Imm e code opération

Format S-Type, utilisé pour les instructions de stockage, 12 bits immédiat.

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 avec ces formats, toutes ces instructions sont de 32 bits et les 7 bits faibles dans chaque format correspond au champ opcode. Notez également que même si plusieurs formats ont des instantanés de la même taille, leurs bits proviennent différentes parties de l'instruction. Comme nous le verrons, le décodeur binaire de spécification peut exprimer cela.

Description de l'encodage binaire

L'encodage binaire de l'instruction est exprimé au format binaire (.bin_fmt). Il décrit l'encodage binaire du des instructions dans une ISA, afin qu'un décodeur d'instructions au format binaire puisse être générées. Le décodeur généré détermine l'opération, extrait la valeur opérande et champs immédiats, afin de fournir les informations requises par l'ISA le décodeur indépendant de l'encodage décrit dans le tutoriel précédent.

Dans ce tutoriel, nous allons écrire un fichier de description de l'encodage binaire pour un sous-ensemble des instructions RiscV32I nécessaires pour simuler les instructions utilisées dans un petit "Hello World" programme. Pour en savoir plus sur l'ISA RiscV, consultez 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 du décodeur spécifie le nom du 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, opcode_enum, qui nomme la façon dont le type d'énumération 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 pour le code généré pour ce décodeur. Dans notre cas, il s'agit du fichier produit par le décodeur ISA à partir du dans le tutoriel précédent. Vous pouvez spécifier des fichiers d'inclusion supplémentaires dans un includes {} à portée globale. définition. Cela s'avère utile lorsque plusieurs décodeurs sont définis et qu'ils ont tous besoin pour inclure certains de ces mêmes fichiers. Quatrièmement, une liste de noms d'instruction qui constituent les instructions pour lesquelles le décodeur est généré. Dans notre au cas où il n'y en avait qu'un: RiscVInst32.

Ensuite, il y a trois définitions de format. Il s'agit de différentes instructions formats pour un mot d'instruction de 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 contient deux champs: bits (largeur de 25 bits) et opcode (largeur de 7 bits). Chaque champ est unsigned, ce qui signifie que la valeur sera étendue à zéro lors de son extraction. et placée dans un type entier C++. La somme des largeurs des champs de bits doit égale à la largeur du format. L'outil génère une erreur désaccord. Ce format ne provient d'aucun autre format. Il est donc est considéré comme un format de premier niveau.

La seconde définit un format d'instruction de 32 bits nommé IType, qui dérive de Inst32Format, ce qui rend ces deux formats liés. Le format contient cinq : imm12, rs1, func3, rd et opcode. Le champ imm12 est signed, ce qui signifie que la valeur est complétée par un signe lorsqu'elle est extrait et placé dans un type entier C++. Notez que IType.opcode a toutes les deux le même attribut signé/non signé et fait référence aux mêmes bits de mot d'instruction en tant que Inst32Format.opcode.

Le troisième format est un format personnalisé qui n'est utilisé que par la propriété fence qui est déjà spécifiée, mais qui n'est pas à vous soucier dans ce tutoriel.

Important: Réutilisez les noms de champs dans différents formats associés, à condition qu'ils représentent les mêmes bits et ont le même attribut signé/non signé.

Un groupe d'instructions apparaît après les définitions de format dans riscv32i.bin_fmt. définition. Toutes les instructions d'un groupe d'instructions doivent avoir le même et utilisent un format qui dérive (peut-être indirectement) de la même un format d'instruction de premier niveau. Lorsqu’une ISA peut avoir des instructions avec différentes un groupe d'instructions différent est utilisé pour chaque longueur. En outre, si le décodage ISA cible dépend d'un mode d'exécution, tel que Arm ou Thumb d'instructions, un groupe d'instructions distinct est requis pour chaque mode. La 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 le type d'énumération OPcode à utiliser "OpcodeEnum", et une instruction de base . Le type d'énumération OPcode doit être identique à celui produit par d'instructions indépendantes de format abordées dans le tutoriel sur l'ISA décodeur.

La description de l'encodage de chaque instruction se compose de trois parties:

  • Le nom de l'opération, qui doit être identique à celui utilisé dans l'instruction du décodeur pour qu'ils fonctionnent ensemble.
  • Format d'instruction à utiliser pour le code d'opération. Il s'agit du format qui est utilisé pour satisfaire les références aux champs de bits dans la partie finale.
  • Une liste de contraintes de champ de bits séparées par une virgule, ==, !=, <, <=, >, et >= qui doivent toutes être vraies pour que le code d'opération corresponde correctement au de l'instruction.

L'analyseur .bin_fmt utilise toutes ces informations pour créer un décodeur qui:

  • Fournit des fonctions d'extraction (signées/non signées) appropriées pour chaque bit dans tous les formats. Les fonctions d'extracteur sont placées dans des espaces de noms nommée par la version snake-case du nom de format. Par exemple, le paramètre les fonctions d'extracteur au format IType sont placées dans l'espace de noms i_type. Chaque fonction d'extracteur est déclarée inline et prend la uint_t la plus étroite qui contient la largeur du format et renvoie la valeur int_t la plus étroite (pour les signatures signées), uint_t (pour les entités non signées), qui contient le champ extrait la largeur. Par exemple :
inline uint8_t ExtractOpcode(uint32_t value) {
  return value & 0x7f;
}
  • Une fonction de décodage pour chaque groupe d'instructions. Elle renvoie une valeur de type OpcodeEnum, et utilise le type uint_t le plus étroit contenant la largeur de le format du groupe d'instructions.

Exécuter la compilation initiale

Remplacez le répertoire par riscv_bin_decoder et compilez le projet à l'aide de la méthode la commande suivante:

$ cd riscv_bin_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_bin_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_bin_decoder
  • riscv32i_bin_decoder.h
  • riscv32i_bin_decoder.cc

Le fichier d'en-tête généré (.h)

Ouvrez riscv32i_bin_decoder.h. La première partie du fichier contient Protections standards, fichiers inclus et déclarations d'espace de noms Ensuite, une fonction d'assistance modélisée se trouve dans l'espace de noms internal. Cette fonction est utilisé pour extraire les champs de bits de formats trop longs pour tenir dans un C++ 64 bits entier.

#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 chaque des déclarations format dans le 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 dans ce format est défini. De plus, le format de base duplique les fonctions d'extraction des formats descendants pour lesquels 1) les noms de champ ne figurent que dans un seul nom de champ, ou 2) pour lesquels le les noms de champ font référence au même champ de type (positions de bits signées/non signées) dans chaque format dans lequel elles apparaissent. Cela permet d'utiliser des champs de bits décrivant bits à extraire à l'aide de fonctions dans l'espace de noms du format de premier niveau.

Les fonctions de l'espace de noms i_type sont présenté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 la fonction du décodeur pour l'instruction le groupe RiscVInst32 est déclaré. Il prend un 32 bits non signés comme valeur l'instruction et renvoie le membre de la classe d'énumération OpcodeEnum. qui correspond ou OpcodeEnum::kNone en l'absence de correspondance.

OpcodeEnum DecodeRiscVInst32(uint32_t inst_word);

Fichier source généré (.cc)

Ouvrez maintenant riscv32i_bin_decoder.cc. La première partie du fichier contient Les déclarations #include et d'espace de noms, suivies de la fonction du décodeur déclarations:

#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 qui renvoient OpcodeEnum::kNone. Les trois autres fonctions constituent généré par un décodeur. Le décodeur global fonctionne de manière hiérarchique. Un ensemble de bits dans le mot d'instruction est calculé pour différencier ou des groupes d'instructions de niveau supérieur. Il n'est pas nécessaire que les bits être contiguës. Le nombre de bits détermine la taille d'un tableau de conversion qui contient des fonctions de décodeur de deuxième niveau. Nous verrons cela dans 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 du 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, lorsqu'il n'y a que 4 instructions définies, un seul niveau de décodage et une table de conversion très creuse. Comme les instructions sont la structure du décodeur change et le nombre de niveaux dans la hiérarchie des tables du décodeur peut augmenter.


Ajouter des instructions ALU d'enregistrement et d'enregistrement

Vous allez maintenant ajouter de nouvelles instructions au fichier riscv32i.bin_fmt. La le premier groupe d'instructions sont des instructions ALU d'enregistrement et d'enregistrement, telles que add, and, etc. Sous RiscV32, ils utilisent tous l'instruction binaire de type R. format:

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

La première chose à faire est d'ajouter le format. Allez-y, ouvrez riscv32i.bin_fmt dans votre éditeur préféré. Juste après le Inst32Format, ajoutez un format appelé RType, dérivé de Inst32Format. Tous les champs de bits dans RType sont unsigned. Utilisez les noms, la largeur en bits et l'ordre (de gauche à droite). du tableau ci-dessus pour définir le format. Si vous avez besoin d'un indice ou voulez voir la solution complète, cliquez ici.

Nous devons ensuite ajouter les instructions. Voici les instructions à suivre:

  • add : addition d'un nombre entier
  • and : 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.

Voici leurs encodages:

31..25 24..20 19..15 14..12 11..7 6... nom du code d'opération
000 000 rs2 rs1 000 e 011 0011 add
000 000 rs2 rs1 111 e 011 0011 et
000 000 rs2 rs1 110 e 011 0011 ou
000 000 rs2 rs1 001 e 011 0011 sll
000 000 rs2 rs1 011 e 011 0011 sltu
010 0000 rs2 rs1 000 e 011 0011 Pub/Sub.
000 000 rs2 rs1 100 e 011 0011 XOR
func7 func3 code opération

Ajoutez ces définitions d'instructions avant les autres instructions dans le Groupe d'instructions RiscVInst32. Les chaînes binaires sont spécifiées préfixe de 0b (semblable à 0x pour les nombres hexadécimaux). Pour faciliter lire de longues chaînes de chiffres binaires, vous pouvez également insérer le guillemet simple ' comme comme séparateur de chiffres.

Chacune de ces définitions d'instructions aura trois contraintes, à savoir func7, func3 et opcode. Pour toutes les entités sauf sub, la contrainte func7 être:

func7 == 0b000'0000

La contrainte func3 varie dans la plupart des instructions. Pour add et sub, c'est:

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 cliquez ici.

Créez maintenant votre projet comme vous l'avez fait précédemment, puis ouvrez le fichier riscv32i_bin_decoder.cc. Vous constaterez que les fonctions de décodeur supplémentaires ont été générés pour gérer les nouvelles instructions. Dans la plupart des cas, ce sont semblables à ceux générés précédemment, mais regardez DecodeRiscVInst32_0_c, 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 extrait de l'instruction pour sélectionner l'index approprié. Cela permet d'ajouter deuxième couche de la hiérarchie du décodeur d'instruction, mais comme le code d'opération peut être sont consultées directement dans un tableau sans effectuer d'autres comparaisons, au lieu d'exiger un autre appel de fonction.


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

Le format I-Type existe déjà dans riscv32i.bin_fmt. Il n'est donc pas nécessaire de ajouter ce format.

Si nous comparons le format spécialisé I-Type au format R-Type que nous avons défini dans Dans l'exercice précédent, nous constatons que la seule différence est que les champs rs2 est renommée uimm5. Au lieu d'ajouter un tout nouveau format, nous pouvons enrichir l'interface R-Type. Impossible d'ajouter un autre champ, car cela augmenterait la largeur de le format, mais nous pouvons ajouter une superposition. Une superposition est un alias pour un ensemble bits au format et peuvent être utilisés pour combiner plusieurs sous-séquences du en une entité nommée distincte. L'effet secondaire est que le code généré inclut désormais une fonction d'extraction pour la superposition, pour les champs. Dans ce cas, lorsque rs2 et uimm5 ne sont pas signés, ne fait pas beaucoup de différence, si ce n'est qu'il faut indiquer explicitement que le champ est utilisé immédiatement. Pour ajouter une superposition nommée uimm5 au format R-Type, ajoutez la méthode 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 prenons le cas des deux instructions qui utilisent ce format: auipc et lui Ces deux éléments décalent 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'image, en transférant un peu de calcul de l'exécution de l'instruction à l'instruction le décodage. 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 bien. Ici, nous la concaténons avec 12 zéros, ce qui permet de la déplacer vers la gauche. par 12.

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.

Voici leurs encodages:

31..20 19..15 14..12 11..7 6... opcode_name
imm12 rs1 000 e 001 0011 Addi
imm12 rs1 111 e 001 0011 andi
imm12 rs1 110 e 001 0011 ori
imm12 rs1 100 e 001 0011 Xori
func3 code opération

Les instructions R-Type (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.

Voici leurs encodages:

31..25 24..20 19..15 14..12 11..7 6... nom du code d'opération
000 000 uimm5 rs1 001 e 001 0011 slli
010 0000 uimm5 rs1 101 e 001 0011 srai
000 000 uimm5 rs1 101 e 001 0011 srli
func7 func3 code opération

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.

Voici leurs encodages:

31..12 11..7 6... nom du code d'opération
uimm20 e 001 0111 auipc
uimm20 e 011 0111 lui
code opération

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.bin_fmt.


La prochaine série d'instructions à définir est la branche conditionnelle les instructions de lancement, l'instruction de création de lien et de lien instruction.

Les branches conditionnelles que nous ajoutons toutes utilisent l'encodage de type B.

31..25 24..20 19..15 14..12 11..7 6...
7 5 5 3 5 7
imm7 rs2 rs1 func3 imm5 code opération

Bien que l'encodage B-Type soit identique à l'encodage R-Type, nous choisissez d'utiliser un nouveau type de format pour qu'il s'aligne sur la documentation RiscV. Mais vous pouvez aussi simplement ajouter une superposition pour obtenir la branche appropriée déplacement immédiat, à l'aide des champs func7 et rd du type R l'encodage.

Il est nécessaire d'ajouter un format BType avec les champs spécifiés ci-dessus, mais ce n'est pas nécessaire. 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 la traitent pas comme une simple concaténation de les deux champs. Au lieu de cela, chaque champ est partitionné davantage, et ces partitions sont concaténés dans un ordre différent. Enfin, cette valeur est décalée vers la gauche pour obtenir un décalage aligné de 16 bits.

La séquence de bits du mot d'instruction utilisé pour former l'instance immédiate est: 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 fichier lsb de imm5

imm7[6], imm5[0], imm7[5..0], imm5[4..1]

L'intégration de cette manipulation de bits dans les instructions de branche elles-mêmes comportent deux de gros inconvénients. Tout d'abord, elle lie l'implémentation de la fonction sémantique à dans la représentation de l'instruction binaire. Deuxièmement, elle augmente la durée d'exécution ou d'autres frais généraux. La solution consiste à ajouter une superposition au format BType, y compris un élément "0" à la fin pour tenir compte du décalage de gauche.

  overlays:
    signed b_imm[13] = imm7[6], imm5[0], imm7[5..0], imm5[4..1], 0b0;

Notez que la superposition est signée et sera donc automatiquement étendue. lorsqu'il est extrait du mot de l'instruction.

L'instruction jump-and-link (immédiate) utilise le codage J-Type:

31..12 11..7 6...
20 5 7
imm20 e code opération

Il s'agit également d'un format facile à ajouter, mais là encore, le format immédiat utilisé par le l'instruction n'est pas aussi simple qu’il y paraît. Les séquences de bits utilisées pour des résultats immédiats complets sont: 31, 19..12, 20, 30..21, et les résultats immédiats finaux sont décalé d'un caractère vers la gauche pour un alignement à la moitié du mot. La solution consiste à ajouter 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 prend en charge la spécification de plusieurs plages dans une dans un format raccourci. De plus, si aucun nom de champ n'est utilisé, le bit les chiffres font référence à l'instruction lui-même, ce qui peut aussi bien être écrit sous la forme:

    signed j_imm[21] = [31, 19..12, 20, 30..21], 0b0;

Enfin, le jump-and-link (enregistre) utilise le format I-type utilisé précédemment.

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

Cette fois, aucune modification ne doit être apportée au format.

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.

Ils sont encodés comme suit:

31..25 24..20 19..15 14..12 11..7 6... nom du code d'opération
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... nom du code d'opération
imm20 e 110 1111 Jal
code opération

L'instruction jalr est encodée comme suit:

31..20 19..15 14..12 11..7 6... opcode_name
imm12 rs1 000 e 110 0111 Jalr
func3 code opération

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.bin_fmt.


Ajouter des instructions pour les magasins

Les instructions du magasin utilisent l'encodage S-Type, qui est identique au type B utilisé par les instructions sur les branches, à l'exception de la composition du immédiatement. Nous avons choisi d'ajouter le format SType pour rester aligné sur le RiscV dans la documentation Google Cloud.

31..25 24..20 19..15 14..12 11..7 6...
7 5 5 3 5 7
imm7 rs2 rs1 func3 imm5 code opération

Dans le cas du format SType, l'expression immédiate est heureusement concaténation directe des deux champs immédiats, de sorte que la spécification de superposition est simplement:

  overlays:
    signed s_imm[12] = imm7, imm5;

Notez qu'aucun spécificateur de plage de bits n'est requis lorsque vous concaténez des champs entiers.

Les instructions du magasin sont encodées comme suit:

31..25 24..20 19..15 14..12 11..7 6... nom du code d'opération
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 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.bin_fmt.


Ajouter des instructions de chargement

Les instructions de chargement utilisent le format I-Type. Aucune modification ne doit y être apportée.

Les encodages sont les suivants:

31..20 19..15 14..12 11..7 6... opcode_name
imm12 rs1 000 e 000 0011 lb
imm12 rs1 100 e 000 0011 lbu
imm12 rs1 001 e 000 0011 m
imm12 rs1 101 e 000 0011 Lhu
imm12 rs1 010 e 000 0011 lw
func3 code opération

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.bin_fmt.

Ce tutoriel est à présent terminé. Nous espérons qu'il vous a été utile.