Décodeur ISA RiscV

Les objectifs de ce tutoriel sont les suivants:

  • Découvrez comment les instructions sont représentées dans le simulateur MPACT-Sim.
  • Découvrez la structure et la syntaxe du fichier de description ISA.
  • Rédiger les descriptions ISA pour le sous-ensemble d'instructions RiscV RV32I

Présentation

Dans MPACT-Sim, les instructions cibles sont décodées et stockées dans un pour rendre leurs informations plus disponibles, et la sémantique plus rapides à exécuter. Ces instances d'instruction sont mises en cache dans une instruction afin de réduire le nombre de fois où des instructions fréquemment exécutées sont exécuté.

Le cours d'instruction

Avant de commencer, il est utile de regarder un peu comment les instructions sont représentées dans MPACT-Sim. La classe Instruction est définie dans mpact-sim/mpact/sim/generic/instruction.h.

L'instance de classe Instruction contient toutes les informations nécessaires pour simuler l'instruction lorsqu'elle est « exécutée », par exemple:

  1. Adresse de l'instruction, taille de l'instruction simulée (par exemple, taille en .text).
  2. Code d'opération de l'instruction.
  3. Pointeur d'interface d'opérande de prédicat (le cas échéant).
  4. Vecteur de pointeurs d'interface d'opérande source.
  5. Vecteur de pointeurs d'interface d'opérande de destination.
  6. Fonction sémantique pouvant être appelée.
  7. Pointeur vers l'objet d'état architectural.
  8. Pointeur vers l'objet de contexte.
  9. Pointeur vers les instances d'instruction enfant et suivantes.
  10. Chaîne de démontage.

Ces instances sont généralement stockées dans un cache d'instructions (instance), et réutilisé chaque fois que l'instruction est réexécutée. Cela permet d'améliorer les performances pendant l'exécution.

À l'exception du pointeur vers l'objet de contexte, tous sont renseignés par la généré à partir de la description ISA. Pour cette il n'est pas nécessaire de connaître le détail de ces éléments, en les utilisant directement. Il est préférable de bien comprendre comment elles sont utilisées. suffisant.

La fonction sémantique appelable est l'objet fonction/méthode/fonction C++ (y compris les lambdas) qui implémente la sémantique de l'instruction. Pour , pour une instruction add, elle charge chaque opérande source, ajoute les deux opérandes et écrit le résultat dans un seul opérande de destination. Le sujet de les fonctions sémantiques est abordée en détail dans le tutoriel sur les fonctions sémantiques.

Opérandes d'instruction

La classe d'instruction comprend des pointeurs vers trois types d'interfaces d'opérande: le prédicat, la source et la destination. Ces interfaces permettent aux fonctions sémantiques être écrit indépendamment du type réel de l'instruction sous-jacente opérande. Par exemple, l'accès aux valeurs des registres et des immédiats se fait via la même interface. Cela signifie que les instructions qui effectuent la même opération mais sur des opérandes différents (par exemple, registres ou instances instantanées) peut être implémentées à l'aide de la même fonction sémantique.

L'interface d'opérande du prédicat, pour les ISA qui acceptent le prédicat d'exécution d'instructions (pour les autres ISA, elle est nulle), est utilisée pour déterminer si une instruction donnée doit s'exécuter en fonction de la valeur booléenne du prédicat.

// The predicte operand interface is intended primarily as the interface to
// read the value of instruction predicates. It is separated from source
// predicates to avoid mixing it in with the source operands needed for modeling
// the instruction semantics.
class PredicateOperandInterface {
 public:
  virtual bool Value() = 0;
  // Return a string representation of the operand suitable for display in
  // disassembly.
  virtual std::string AsString() const = 0;
  virtual ~PredicateOperandInterface() = default;
};

L'interface d'opérande source permet à la fonction sémantique d'instruction de lire valeurs des opérandes d'instructions, sans tenir compte de l'opérande sous-jacent de mots clés. Les méthodes d'interface sont compatibles avec les opérandes scalaires et vectoriels.

// The source operand interface provides an interface to access input values
// to instructions in a way that is agnostic about the underlying implementation
// of those values (eg., register, fifo, immediate, predicate, etc).
class SourceOperandInterface {
 public:
  // Methods for accessing the nth value element.
  virtual bool AsBool(int index) = 0;
  virtual int8_t AsInt8(int index) = 0;
  virtual uint8_t AsUint8(int index) = 0;
  virtual int16_t AsInt16(int index) = 0;
  virtual uint16_t AsUint16(int) = 0;
  virtual int32_t AsInt32(int index) = 0;
  virtual uint32_t AsUint32(int index) = 0;
  virtual int64_t AsInt64(int index) = 0;
  virtual uint64_t AsUint64(int index) = 0;

  // Return a pointer to the object instance that implements the state in
  // question (or nullptr) if no such object "makes sense". This is used if
  // the object requires additional manipulation - such as a fifo that needs
  // to be pop'ed. If no such manipulation is required, nullptr should be
  // returned.
  virtual std::any GetObject() const = 0;

  // Return the shape of the operand (the number of elements in each dimension).
  // For instance {1} indicates a scalar quantity, whereas {128} indicates an
  // 128 element vector quantity.
  virtual std::vector<int> shape() const = 0;

  // Return a string representation of the operand suitable for display in
  // disassembly.
  virtual std::string AsString() const = 0;

  virtual ~SourceOperandInterface() = default;
};

L'interface d'opérande de destination fournit des méthodes d'allocation et de gestion Instances DataBuffer (type de données interne utilisé pour stocker les valeurs de registre). A l'opérande de destination est également associé à une latence, qui correspond au nombre de cycles d'attente jusqu'à ce que l'instance de tampon de données soit allouée par l'instruction est utilisée pour mettre à jour la valeur du registre cible. Pour la latence d'une instruction add peut être de 1, tandis que pour une instruction mpy, pour l'instruction, il peut s'agir de 4. Ce point est abordé plus en détail dans le sur les fonctions sémantiques.

// The destination operand interface is used by instruction semantic functions
// to get a writable DataBuffer associated with a piece of simulated state to
// which the new value can be written, and then used to update the value of
// the piece of state with a given latency.
class DestinationOperandInterface {
 public:
  virtual ~DestinationOperandInterface() = default;
  // Allocates a data buffer with ownership, latency and delay line set up.
  virtual DataBuffer *AllocateDataBuffer() = 0;
  // Takes an existing data buffer, and initializes it for the destination
  // as if AllocateDataBuffer had been called.
  virtual void InitializeDataBuffer(DataBuffer *db) = 0;
  // Allocates and initializes data buffer as if AllocateDataBuffer had been
  // called, but also copies in the value from the current value of the
  // destination.
  virtual DataBuffer *CopyDataBuffer() = 0;
  // Returns the latency associated with the destination operand.
  virtual int latency() const = 0;
  // Return a pointer to the object instance that implmements the state in
  // question (or nullptr if no such object "makes sense").
  virtual std::any GetObject() const = 0;
  // Returns the order of the destination operand (size in each dimension).
  virtual std::vector<int> shape() const = 0;
  // Return a string representation of the operand suitable for display in
  // disassembly.
  virtual std::string AsString() const = 0;
};

Description ISA

L'architecture d'ensemble d'instructions (ISA, Instruction Set Architecture) d'un processeur définit le modèle abstrait par lequel le logiciel interagit avec le matériel. Elle définit l'ensemble les instructions disponibles, les types de données, les registres et d'autres états de machine les instructions, ainsi que leur comportement (sémantique). Aux fins du de MPACT-Sim, l'ISA n'inclut pas l'encodage réel des instructions. Ces données sont traitées séparément.

La norme ISA du processeur est exprimée dans un fichier de description qui décrit d'instructions à un niveau abstrait, sans encodage indépendant. Fichier de description énumère l'ensemble des instructions disponibles. Pour chaque instruction, il est obligatoire d'indiquer son nom, le nombre et les noms de ses opérandes, ainsi que ses à une fonction/appelable C++ qui implémente sa sémantique. En outre, on peut spécifier une chaîne de mise en forme de démontage, et l'utilisation de l'instruction les noms de ressources matérielles. Le premier est utile pour créer un texte représentation de l'instruction de débogage, de traçage ou d'utilisation interactive. La le second permet d'améliorer la précision du cycle dans la simulation.

Le fichier de description ISA est analysé par l’analyseur isa qui génère du code pour le décodeur d'instructions indépendant de la représentation. Ce décodeur est chargé en remplissant les champs des objets d'instruction. Les valeurs spécifiques, disons numéro de registre de destination, sont obtenus à partir d’une instruction spécifique à un format décodeur. L'un de ces décodeurs est le décodeur binaire, qui est l'objet principal tutoriel suivant.

Ce tutoriel explique comment écrire un fichier de description ISA pour une requête scalaire simple de l'architecture. Nous allons utiliser un sous-ensemble de l'ensemble d'instructions RiscV RV32I pour Nous illustrerons cela. Avec les autres tutoriels, vous créerez un simulateur capable de simuler une application "Hello World" programme. Pour en savoir plus sur l'ISA RiscV, consultez Caractéristiques Risc-V

Commencez par ouvrir le fichier: riscv_isa_decoder/riscv32i.isa

Le contenu du fichier est divisé en plusieurs sections. Tout d'abord, l'ISA déclaration:

isa RiscV32I {
  namespace mpact::sim::codelab;
  slots { riscv32; }
}

Cela déclare RiscV32I comme le nom de l'ISA et le générateur de code créez une classe appelée RiscV32IEncodingBase qui définit l'interface généré utilise pour obtenir des informations sur le code d’opération et les opérandes. Le nom de cette classe est générée en convertissant le nom ISA en Pascal-case, puis en le concaténant avec EncodingBase. La déclaration slots { riscv32; } spécifie qu'il n'y a qu'un seul emplacement d'instruction riscv32 dans RiscV32I ISA (par opposition à une instruction VLIW comportant plusieurs emplacements), et que la seule Les instructions valides sont celles qui sont définies pour s'exécuter dans riscv32.

// First disasm fragment is 15 char wide and left justified.
disasm widths = {-15};

Cela indique que le premier fragment de démontage (voir ci-dessous), sera justifié à gauche dans un format de 15 caractères. un champ large. Tous les fragments suivants seront ajoutés à ce champ sans tout espacement supplémentaire.

En dessous, trois déclarations d'emplacement s'affichent: riscv32i, zicsr et riscv32. D'après la définition isa ci-dessus, seules les instructions définies pour riscv32 fera partie de RiscV32I. À quoi servent les deux autres emplacements ?

Les emplacements peuvent être utilisés pour diviser les instructions en groupes distincts, qui peuvent ensuite être dans un seul emplacement à la fin. Notez la notation : riscv32i, zicsr dans la déclaration d'emplacement riscv32. Cela permet de spécifier que l'emplacement riscv32 hérite toutes les instructions définies dans les emplacements zicsr et riscv32i. L'ISA 32 bits RiscV se compose d'une ISA de base appelée RV32I, à laquelle un ensemble d'extensions facultatives peut être ajouté. Le mécanisme d'emplacement permet aux instructions de ces extensions d'être spécifiées séparément, puis combinées si nécessaire à la fin pour définir dans son ensemble. Dans ce cas, les instructions du RiscV "I" groupe sont définis séparément de ceux groupe. Des groupes supplémentaires peuvent être définis pour "M" (multiplier/diviser), "F" (à virgule flottante à simple précision), "D" (à virgule flottante à double précision), "C" (instructions compactes 16 bits), etc. comme nécessaires pour l'ISA RiscV finale souhaitée.

// The RiscV 'I' instructions.
slot riscv32i {
  ...
}

// RiscV32 CSR manipulation instructions.
slot zicsr {
  ...
}

// The final instruction set combines riscv32i and zicsr.
slot riscv32 : riscv32i, zicsr {
  ...
}

Il n'est pas nécessaire de modifier les définitions des emplacements zicsr et riscv32. Toutefois, Ce tutoriel vise à ajouter les définitions nécessaires à riscv32i. emplacement. Examinons de plus près les éléments actuellement définis dans cet emplacement:

// The RiscV 'I' instructions.
slot riscv32i {
  // Include file that contains the declarations of the semantic functions for
  // the 'I' instructions.
  includes {
    #include "learning/brain/research/mpact/sim/codelab/riscv_semantic_functions/solution/rv32i_instructions.h"
  }
  // These are all 32 bit instructions, so set default size to 4.
  default size = 4;
  // Model these with 0 latency to avoid buffering the result. Since RiscV
  // instructions have sequential semantics this is fine.
  default latency = 0;
  // The opcodes.
  opcodes {
    fence{: imm12 : },
      semfunc: "&RV32IFence"c
      disasm: "fence";
    ebreak{},
      semfunc: "&RV32IEbreak",
      disasm: "ebreak";
  }
}

Tout d'abord, il y a une section includes {} qui répertorie les fichiers d'en-tête qui ont besoin à inclure dans le code généré lorsque cet emplacement est référencé, directement ou indirectement, dans la ISA finale. Les fichiers inclus peuvent également être répertoriés dans une liste globale une section includes {} limitée, auquel cas elles sont toujours incluses. Cela peut s'avère pratique si le même fichier d'inclusion doit normalement être ajouté à chaque emplacement définition.

Les déclarations default size et default latency définissent que, sauf si autrement, la taille d'une instruction est de 4, et la latence d'une d'écriture de l'opérande de destination a zéro cycle. Notez que la taille de l'instruction spécifiée ici, est la taille de l'incrément de compteur du programme pour calculer le adresse de l'instruction séquentielle suivante à exécuter dans l'instance processeur. Elle peut être identique ou non à la taille en octets du fichier d'instructions dans le fichier exécutable d'entrée.

La section "OPcode" se trouve au centre de la définition de l'emplacement. Comme vous pouvez le voir, seuls deux les codes d'opérations (instructions) fence et ebreak ont été définis jusqu'à présent dans riscv32i Le code opérationnel fence est défini en spécifiant le nom (fence) et la spécification de l'opérande ({: imm12 : }), suivie du démontage (facultatif) ; ("fence"), et l'appelable qui doit être lié en tant que ("&RV32IFence").

Les opérandes d'instruction sont spécifiés sous la forme d'un triple, chaque composant Séparé par un point-virgule, predicate ':' Liste d'opérandes source ':' liste d'opérandes de destination. Les listes d'opérandes source et de destination doivent être séparées par une virgule des listes de noms d'opérandes séparées. Comme vous pouvez le voir, les opérandes d'instruction pour l'instruction fence ne contient aucun opérande de prédicat, une seule source ; nom d'opérande imm12, et aucun opérande de destination. Le sous-ensemble RiscV RV32I fait ne prennent pas en charge l'exécution prédite. L'opérande de prédicat sera donc toujours vide. dans ce tutoriel.

La fonction sémantique est spécifiée en tant que chaîne nécessaire pour spécifier le code C++ ou appelable à utiliser pour appeler la fonction sémantique. La signature du fonction sémantique/appelable est void(Instruction *).

La spécification de démontage consiste en une liste de chaînes séparées par une virgule. En règle générale, seules deux chaînes sont utilisées, une pour le code d'opération et une pour le opérandes. Une fois formaté (à l'aide de l'appel AsString() dans l'instruction), chaque chaîne est formatée dans un champ conformément au disasm widths décrites ci-dessus.

Les exercices suivants vous aident à ajouter des instructions au fichier riscv32i.isa. pour simuler une application "Hello World" programme. Pour ceux qui sont pressés, solutions se trouvent dans riscv32i.isa et rv32i_instructions.h.


Effectuer la compilation initiale

Si vous n'avez pas remplacé le répertoire par riscv_isa_decoder, faites-le maintenant. Ensuite, Compilez le projet comme suit. La compilation devrait réussir.

$ cd riscv_isa_decoder
$ bazel build :all

Remettez votre répertoire à la racine du dépôt, puis voyons aux sources générées. Pour cela, remplacez le répertoire par bazel-out/k8-fastbuild/bin/riscv_isa_decoder (en supposant que vous utilisez un ordinateur x86 host - pour les autres hôtes, k8-fastbuild sera une autre chaîne).

$ cd ..
$ cd bazel-out/k8-fastbuild/bin/riscv_isa_decoder

Ce répertoire contient, entre autres fichiers, les éléments suivants : des fichiers C++ générés:

  • riscv32i_decoder.h
  • riscv32i_decoder.cc
  • riscv32i_enums.h
  • riscv32i_enums.cc

Cliquez dessus dans le navigateur pour examiner riscv32i_enums.h. Vous devez vous verrez qu'elle contient quelque chose comme:

#ifndef RISCV32I_ENUMS_H
#define RISCV32I_ENUMS_H

namespace mpact {
namespace sim {
namespace codelab {
  enum class SlotEnum {
    kNone = 0,
    kRiscv32,
  };

  enum class PredOpEnum {
    kNone = 0,
    kPastMaxValue = 1,
  };

  enum class SourceOpEnum {
    kNone = 0,
    kCsr = 1,
    kImm12 = 2,
    kRs1 = 3,
    kPastMaxValue = 4,
  };

  enum class DestOpEnum {
    kNone = 0,
    kCsr = 1,
    kRd = 2,
    kPastMaxValue = 3,
  };

  enum class OpcodeEnum {
    kNone = 0,
    kCsrs = 1,
    kCsrsNw = 2,
    kCsrwNr = 3,
    kEbreak = 4,
    kFence = 5,
    kPastMaxValue = 6
  };

  constexpr char kNoneName[] = "none";
  constexpr char kCsrsName[] = "Csrs";
  constexpr char kCsrsNwName[] = "CsrsNw";
  constexpr char kCsrwNrName[] = "CsrwNr";
  constexpr char kEbreakName[] = "Ebreak";
  constexpr char kFenceName[] = "Fence";
  extern const char *kOpcodeNames[static_cast<int>(
      OpcodeEnum::kPastMaxValue)];

  enum class SimpleResourceEnum {
    kNone = 0,
    kPastMaxValue = 1
  };

  enum class ComplexResourceEnum {
    kNone = 0,
    kPastMaxValue = 1
  };

  enum class AttributeEnum {
    kPastMaxValue = 0
  };

}  // namespace codelab
}  // namespace sim
}  // namespace mpact

#endif  // RISCV32I_ENUMS_H

Comme vous pouvez le voir, chaque emplacement, code d'opération et opérande définis dans la section riscv32i.isa est défini dans l'un des types d'énumération. En outre, il existe un tableau OpcodeNames qui stocke tous les noms des opcodes (il s'agit défini dans riscv32i_enums.cc). Les autres fichiers contiennent le décodeur généré, que nous aborderons plus en détail dans un autre tutoriel.

Règle de compilation Bazel

Dans Bazel, la cible du décodeur ISA est définie à l'aide d'une macro de règle personnalisée nommée mpact_isa_decoder, chargé depuis mpact/sim/decoder/mpact_sim_isa.bzl dans le dépôt mpact-sim. Pour ce tutoriel, la cible de compilation définie dans riscv_isa_decoder/BUILD est:

mpact_isa_decoder(
    name = "riscv32i_isa",
    src = "riscv32i.isa",
    includes = [],
    isa_name = "RiscV32I",
    deps = [
        "//riscv_semantic_functions:riscv32i",
    ],
)

Cette règle appelle l'outil d'analyse et le générateur ISA pour générer le code C++, le code généré est ensuite compilé dans une bibliothèque dont d'autres règles peuvent dépendre l'étiquette //riscv_isa_decoder:riscv32i_isa. La section includes est utilisée pour spécifier des fichiers .isa supplémentaires que le fichier source peut inclure. La isa_name permet de spécifier une autorité de certification spécifique, obligatoire s'il y en a plusieurs spécifié, dans le fichier source pour lequel générer le décodeur.


Ajouter des instructions ALU d'enregistrement et d'enregistrement

Vous allez maintenant ajouter de nouvelles instructions au fichier riscv32i.isa. Le premier L'ensemble d'instructions est constitué d'instructions ALU d'enregistrement et d'enregistrement, telles que add, and, etc. Sous RiscV32, ils utilisent tous le format d'instruction binaire de type R:

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

Bien que le fichier .isa soit utilisé pour générer un décodeur indépendant du format, il est il est utile de tenir compte du format binaire et de sa mise en page pour guider les entrées. En Vous voyez qu'il y a trois champs pertinents pour le décodeur qui renseigne objets d'instruction: rs2, rs1 et rd. À ce stade, nous choisissons d'utiliser ces noms pour les registres d'entiers qui sont encodés de la même manière (séquences de bits), dans les mêmes champs d'instruction, dans toutes les instructions.

Les instructions que nous allons ajouter sont les suivantes:

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

Chacune de ces instructions sera ajoutée à la section opcodes du Définition de l'emplacement riscv32i. Rappelez-vous que nous devons spécifier le nom, les opcodes, le démontage et la fonction sémantique de chaque instruction. Le nom est facile, utilisons simplement les noms d’opération ci-dessus. De plus, ils utilisent tous les mêmes opérandes, donc nous pouvons utiliser { : rs1, rs2 : rd} pour la spécification de l'opérande. Cela signifie que l'opérande source du registre spécifié par rs1 aura l'index 0 dans la source vecteur d'opérande dans l'objet d'instruction, l'opérande d'enregistrement source spécifié par rs2 auront l'index 1 et l'opérande de destination du registre spécifié par rd sera le seul élément dans le vecteur d'opérande de destination (à l'index 0).

Vient ensuite la spécification de la fonction sémantique. Pour ce faire, vous pouvez utiliser le mot clé semfunc et une chaîne C++ spécifiant un appelable pouvant être utilisé pour attribuer à un std::function. Dans ce tutoriel, nous utiliserons des fonctions. Le code pouvant être appelé la chaîne sera "&MyFunctionName". En utilisant le schéma de dénomination suggéré Instruction fence. Il doit s'agir de "&RV32IAdd", "&RV32IAnd", etc.

Enfin, les spécifications de démontage. Elle commence par le mot clé disasm et est suivie d'une liste de chaînes séparées par une virgule spécifiant comment doit s'afficher sous forme de chaîne. Utiliser un signe % devant un nom de l'opérande indique une substitution de chaîne à l'aide de la représentation sous forme de chaîne de cet opérande. Pour l'instruction add, le format est le suivant: disasm: "add", "%rd, %rs1,%rs2". Cela signifie que l'entrée de l'instruction add doit ressembler par exemple:

    add{ : rs1, rs2 : rd},
      semfunc: "&RV32IAdd",
      disasm: "add", "%rd, %rs1, %rs2";

Modifiez le fichier riscv32i.isa et ajoutez toutes ces instructions au Description de .isa. Si vous avez besoin d'aide (ou si vous souhaitez vérifier votre travail), l'intégralité le fichier de description est cliquez ici.

Une fois les instructions ajoutées au fichier riscv32i.isa, il sera nécessaire d'ajouter des déclarations pour chacune des nouvelles fonctions sémantiques référencée au fichier rv32i_instructions.h situé dans '../semantic_functions/. Encore une fois, si vous avez besoin d'aide (ou souhaitez vérifier votre travail), la réponse est cliquez ici.

Une fois que c'est fait, continuez et revenez à riscv_isa_decoder et de le recompiler. N'hésitez pas à examiner les fichiers sources générés.


Ajouter des instructions ALU avec des résultats immédiats

La prochaine série d'instructions que nous ajouterons concerne les instructions ALU qui utilisent un une valeur immédiate au lieu de l'un des registres. Il existe trois groupes de ces instructions (basées sur le champ immédiat): les instructions immédiates I-Type avec une signature immédiate de 12 bits, les instructions spécialisées I-Type immédiates pour les décalages et le type U immédiate, avec une valeur immédiate non signée de 20 bits. Les formats sont présentés ci-dessous:

Le format I-Type immédiat:

31..20 19..15 14..12 11..7 6...
12 5 3 5 7
imm12 rs1 func3 e code opération

Le format I-Type spécialisé immédiat:

31..25 24..20 19..15 14..12 11..7 6...
7 5 5 3 5 7
func7 uimm5 rs1 func3 e code opération

Format immédiat de type U:

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

Comme vous pouvez le voir, les noms d'opérande rs1 et rd font référence aux mêmes champs de bits que précédemment, et sont utilisés pour représenter des registres d'entiers, donc ces noms peuvent être et conservés. Les champs de valeur immédiate ont une longueur et un emplacement différents. deux (uimm5 et uimm20) ne sont pas signées, tandis que imm12 est signée. Chacun de ces éléments celles-ci utiliseront leur propre nom.

Les opérandes des instructions de type I doivent donc être { : rs1, imm12 :rd }. Pour les instructions spécialisées I-Type, il doit s'agir de { : rs1, uimm5 : rd}. La spécification d'opérande de l'instruction de type U doit être { : uimm20 : rd }.

Les instructions I-Type que nous devons ajouter sont les suivantes:

  • addi : ajouter immédiatement.
  • andi : au niveau du bit et avec immédiate.
  • ori : au niveau du bit ou avec immédiate.
  • xori : valeur xor au niveau du bit avec immédiat.

Les instructions spécialisées I-Type que nous devons ajouter sont les suivantes:

  • slli : décalage immédiat de la logique vers la gauche.
  • srai : décale l'arithmétique vers la droite de manière immédiate.
  • srli : décale immédiatement la logique vers la droite.

Les instructions de type U que nous devons ajouter sont les suivantes:

  • auipc : ajoute la valeur "upper" immédiate au PC.
  • lui : charge la partie supérieure immédiatement.

Les noms à utiliser pour les opcodes découlent naturellement des noms des instructions ci-dessus (inutile d'en créer de nouveaux, ils sont tous uniques). Quand il s'agit de spécifiant les fonctions sémantiques, rappelez-vous que les objets d'instruction encodent Des interfaces aux opérandes sources qui sont indépendantes de l'opérande sous-jacent de mots clés. Cela signifie que pour les instructions ayant la même opération, mais peuvent différer en types d'opérandes et partager la même fonction sémantique. Par exemple, l'instruction addi effectue la même opération que l'instruction add si le type d'opérande est ignoré. Ils peuvent donc utiliser la même fonction la spécification "&RV32IAdd". Il en va de même pour andi, ori, xori et slli. Les autres instructions utilisent de nouvelles fonctions sémantiques, mais elles doivent être nommées en fonction de l'opération, et non des opérandes. Pour srai, utilisez donc "&RV32ISra". La Les instructions de type U auipc et lui n'ont pas d'équivalents dans le registre. Elles sont donc acceptées. pour utiliser "&RV32IAuipc" et "&RV32ILui".

Les cordes de démontage sont très semblables à celles de l'exercice précédent, mais comme prévu, les références à %rs2 sont remplacées par %imm12, %uimm5, ou %uimm20, selon le cas.

Apportez les modifications nécessaires et continuez. Vérifiez le résultat généré. Tout comme auparavant, vous pouvez comparer votre travail riscv32i.isa et rv32i_instructions.h.


Les instructions de branche et de saut et de lien que nous devons ajouter utilisent toutes les deux une destination opérande qui n'est implicite que dans l'instruction elle-même, à savoir le prochain PC . À ce stade, nous la traiterons comme un opérande approprié portant le nom next_pc Elle sera définie plus en détail dans un tutoriel ultérieur.

Instructions pour l'agence

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

31 30..25 24..20 19..15 14..12 11..8 7 6...
1 6 5 5 3 4 1 7
Imm Imm rs2 rs1 func3 Imm Imm code opération

Les différents champs de données immédiates sont concaténés en un mot clé de requête immédiate de 12 bits . Étant donné que le format n'est pas vraiment pertinent, nous l'appellerons immédiatement bimm12, pour une branche 12 bits immédiate. La fragmentation sera traitée le prochain tutoriel sur la création du décodeur binaire. Toutes les les instructions de branchement comparent les registres d'entiers spécifiés par rs1 et rs2, si la condition est vraie, la valeur immédiate est ajoutée à la valeur pc actuelle pour génère l'adresse de l'instruction suivante à exécuter. Les opérandes du les instructions de branche doivent donc être { : rs1, rs2, bimm12 : next_pc }.

Les instructions concernant la branche que nous devons ajouter sont les suivantes:

  • beq : branche si elle est égale.
  • bge : branche si elle est supérieure ou égale à.
  • bgeu : branche si supérieure ou égale à non signée.
  • blt : branche si la valeur est inférieure à.
  • bltu : branche si la valeur n'est pas déjà signée.
  • bne : branche si différente.

Ces noms d'opération sont tous uniques et peuvent donc être réutilisés dans le .isa. la description. Bien entendu, vous devez ajouter de nouveaux noms de fonctions sémantiques, comme "&RV32IBeq", etc.

La spécification de démontage est désormais un peu plus délicate, puisque l'adresse du est utilisée pour calculer la destination, sans qu'elle ne fasse partie des opérandes d'instruction. Cependant, il fait partie des informations stockées dans l'objet d'instruction pour qu'il soit disponible. La solution consiste à utiliser dans la chaîne de désassemblage. Au lieu d'utiliser "%" suivi de le nom de l'opérande, vous pouvez saisir %(expression: format d'impression). Très simple les expressions exactes sont acceptées, mais address + offset est l'une d'entre elles, avec @ symbole utilisé pour l'adresse d'instruction actuelle. Le format d'impression est semblable à formats printf de style C, mais sans % de début. Le format de démontage de l'instruction beq devient alors:

    disasm: "beq", "%rs1, %rs2, %(@+bimm12:08x)"

Vous n'avez besoin d'ajouter que deux instructions pour utiliser la fonctionnalité Jump and Link : jal (jump-and-link) et jalr (saut et lien indirects).

L'instruction jal utilise l'encodage J-Type:

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

Comme pour les instructions sur la branche, l'immédiate de 20 bits est fragmentée plusieurs champs. Nous allons donc l'appeler jimm20. La fragmentation n'est pas importante pour le moment, mais nous y reviendrons sur la création du décodeur binaire. Opérande devient alors { : jimm20 : next_pc, rd }. Notez qu'il existe deux opérandes de destination, la valeur pc suivante et le registre de liens spécifié dans la instruction.

Comme dans les instructions ci-dessus pour les branches, le format de démontage est le suivant:

    disasm: "jal", "%rd, %(@+jimm20:08x)"

Le jump-and-link indirect utilise le format I-Type avec le caractère immédiat de 12 bits. Il ajoute la valeur immédiate étendu par signe au registre d'entiers spécifié par rs1 pour générer l'adresse d'instruction cible. Le registre de liens est registre d'entiers spécifié par rd.

31..20 19..15 14..12 11..7 6...
12 5 3 5 7
imm12 rs1 func3 e code opération

Si vous avez vu le motif, vous devez en déduire que la spécification de l'opérande pour jalr doit être { : rs1, imm12 : next_pc, rd }, et l'état de démontage caractéristiques:

    disasm: "jalr", "%rd, %rs1, %imm12"

Apportez les modifications nécessaires, puis créez votre solution. Vérifiez le résultat généré. Juste comme précédemment, vous pouvez comparer votre travail riscv32i.isa et rv32i_instructions.h.


Ajouter des instructions concernant le magasin

Les instructions du magasin sont très simples. Ils utilisent tous le format S-Type:

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

Comme vous pouvez le voir, il s'agit encore d'un autre cas d'imminence fragmentée de 12 bits. appelez-le simm12. Les instructions du magasin stockent toutes la valeur de l'entier de registre spécifié par rs2 à l'adresse effective en mémoire obtenue en ajoutant la valeur du registre d'entiers spécifié par rs1 à la valeur de signe étendu de l'immédiat 12 bits. L'opérande doit être au format { : rs1, simm12, rs2 } pour toutes les instructions du magasin.

Voici les instructions à implémenter pour le magasin:

  • sb : octet de magasin.
  • sh : stocke un demi-mot.
  • sw : stocke le mot.

Les spécifications de démontage de sb sont les suivantes:

    disasm: "sb", "%rs2, %simm12(%rs1)"

Les spécifications de la fonction sémantique sont également ce à quoi vous vous attendiez: "&RV32ISb", etc.

Apportez les modifications nécessaires, puis créez votre solution. Vérifiez le résultat généré. Juste comme précédemment, vous pouvez comparer votre travail riscv32i.isa et rv32i_instructions.h.


Ajouter des instructions de chargement

Les instructions de chargement sont modélisées un peu différemment des autres instructions dans le simulateur. Pour modéliser les cas où la latence de chargement est incertain, les instructions de chargement sont divisées en deux actions distinctes: 1) effective le calcul d'adresse et l'accès à la mémoire, et 2) l'écriture des résultats. Dans s'effectue en divisant l'action sémantique de la charge en des instructions distinctes, l'instruction principale et une instruction enfant. De plus, lorsque nous spécifions des opérandes, nous devons les spécifier à la fois pour l'opérande principal et pour instruction child. Pour ce faire, la spécification de l'opérande est traitée liste de triplés. La syntaxe est la suivante:

{(predicate : sources : destinations), (predicate : sources : destinations), ... }

Les instructions de chargement utilisent toutes le format I-Type comme la plupart des instructions:

31..20 19..15 14..12 11..7 6...
12 5 3 5 7
imm12 rs1 func3 e code opération

La spécification d'opérande divise les opérandes nécessaires au calcul de l'adresse. et lancez l'accès à la mémoire à partir de la destination du registre pour les données de chargement: {( : rs1, imm12 : ), ( : : rd) }

Comme l'action sémantique est divisée en deux instructions, les fonctions sémantiques de même que deux appelsables. Pour lw (charger le mot), il s'agit de la formule suivante : écrit:

    semfunc: "&RV32ILw", "&RV32ILwChild"

Les spécifications de démontage sont plus conventionnelles. Aucune mention n'est faite du pour les enfants. Pour lw, il doit s'agir de:

    disasm: "lw", "%rd, %imm12(%rs1)"

Voici les instructions de chargement à implémenter:

  • lb : octet de chargement.
  • lbu : octet de chargement non signé.
  • lh : charge le demi-mot.
  • lhu : charge un demi-mot non signé.
  • lw : chargez le mot.

Apportez les modifications nécessaires, puis créez votre solution. Vérifiez le résultat généré. Juste comme précédemment, vous pouvez comparer votre travail riscv32i.isa et rv32i_instructions.h.

Merci d'être arrivé jusqu'ici. Nous espérons que ces informations vous ont été utiles.