Décodeur intégré RiscV

Les objectifs de ce tutoriel sont les suivants:

  • Découvrez comment l'ISA générée et les décodeurs binaires s'assemblent.
  • Écrire le code C++ nécessaire pour créer un décodeur d'instructions complet pour RiscV RV32I qui combine les décodeurs ISA et binaires.

Comprendre le décodeur d'instructions

Le décodeur d'instructions est chargé, en fonction d'une adresse d'instruction, de lire le mot d'instruction de la mémoire et renvoyer une instance entièrement initialisée du Instruction qui représente cette instruction.

Le décodeur de niveau supérieur implémente l'élément generic::DecoderInterface présenté ci-dessous:

// This is the simulator's interface to the instruction decoder.
class DecoderInterface {
 public:
  // Return a decoded instruction for the given address. If there are errors
  // in the instruciton decoding, the decoder should still produce an
  // instruction that can be executed, but its semantic action function should
  // set an error condition in the simulation when executed.
  virtual Instruction *DecodeInstruction(uint64_t address) = 0;
  virtual ~DecoderInterface() = default;
};

Comme vous pouvez le voir, il n'y a qu'une seule méthode à implémenter: cpp virtual Instruction *DecodeInstruction(uint64_t address);

Voyons maintenant les éléments fournis et les éléments nécessaires au code généré.

Tout d'abord, considérons la classe de premier niveau RiscV32IInstructionSet dans le fichier. riscv32i_decoder.h, généré à la fin du tutoriel sur la décodeur ISA. Pour revoir le contenu, accédez au répertoire des solutions ce tutoriel et tout recompiler.

$ cd riscv_isa_decoder/solution
$ bazel build :all
...<snip>...

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

Les quatre fichiers sources contenant le code C++ généré s'affichent:

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

Ouvrez le premier fichier riscv32i_decoder.h. Il y a trois classes que nous vous devez examiner:

  • RiscV32IEncodingBase
  • RiscV32IInstructionSetFactory
  • RiscV32IInstructionSet

Notez la dénomination des classes. Toutes les classes sont nommées d'après le nom Version du nom indiqué dans le texte "isa" (pascal, en majuscules et minuscules) dans ce fichier: isa RiscV32I { ... }

Commençons par la classe RiscVIInstructionSet. Elle est illustrée ci-dessous:

class RiscV32IInstructionSet {
 public:
  RiscV32IInstructionSet(ArchState *arch_state,
                         RiscV32IInstructionSetFactory *factory);
  Instruction *Decode(uint64 address, RiscV32IEncodingBase *encoding);

 private:
  std::unique_ptr<Riscv32Slot> riscv32_decoder_;
  ArchState *arch_state_;
};

Il n'y a pas de méthodes virtuelles dans cette classe. Il s'agit donc d'une classe autonome, mais vous remarquerez deux choses. Tout d'abord, le constructeur utilise un pointeur vers une instance de RiscV32IInstructionSetFactory. Il s'agit d'une classe que le decoder utilise pour créer une instance de la classe RiscV32Slot, qui permet de décoder toutes les instructions définies pour slot RiscV32, telles que définies dans riscv32i.isa. Deuxièmement, la méthode Decode utilise un paramètre supplémentaire de type pointeur vers RiscV32IEncodingBase, il s'agit d'une classe qui fournira entre le décodeur ISA généré dans le premier tutoriel et le fichier binaire généré dans le deuxième atelier.

La classe RiscV32IInstructionSetFactory est une classe abstraite à partir de laquelle nous nous devons obtenir notre propre implémentation pour le décodeur complet. Dans la plupart des cas, est simple: il vous suffit de fournir une méthode permettant d'appeler le constructeur pour chaque d'emplacement définie dans le fichier .isa. Dans notre cas, c'est très simple, car n'est qu'une seule de ce type de classe: Riscv32Slot (pascal en casse du nom riscv32 concaténé avec Slot). La méthode n'est pas générée automatiquement certains cas d'utilisation avancés pour lesquels il peut être utile de dériver une sous-classe à partir de l'emplacement et appeler son constructeur à la place.

Nous aborderons la dernière classe RiscV32IEncodingBase plus tard dans ce car c'est le sujet d'un autre exercice.


Définir un décodeur d'instructions de premier niveau

Définir la classe de fabrique

Si vous avez recréé le projet pour le premier tutoriel, veillez à revenir à le répertoire riscv_full_decoder.

Ouvrez le fichier riscv32_decoder.h. Tous les fichiers d'inclusion nécessaires ont déjà été ajoutés et que les espaces de noms ont été configurés.

Après le commentaire marqué //Exercise 1 - step 1, définissez la classe RiscV32IsaFactory héritant de RiscV32IInstructionSetFactory.

class RiscV32IsaFactory : public RiscV32InstructionSetFactory {};

Ensuite, définissez le forçage pour CreateRiscv32Slot. Puisque nous n'utilisons aucune dérivées de Riscv32Slot, il nous suffit d'allouer une nouvelle instance à l'aide de std::make_unique

std::unique_ptr<Riscv32Slot> CreateRiscv32Slot(ArchState *) override {
  return std::make_unique<Riscv32Slot>(state);
}

Si vous avez besoin d'aide (ou si vous souhaitez vérifier votre travail), la réponse complète est cliquez ici.

Définir la classe du décodeur

Constructeurs, destructeurs et déclarations de méthode

Vous devez maintenant définir la classe du décodeur. Dans le même fichier que celui indiqué ci-dessus, accédez à déclaration de RiscV32Decoder. Développer la déclaration en une définition de classe où RiscV32Decoder hérite de generic::DecoderInterface.

class RiscV32Decoder : public generic::DecoderInterface {
  public:
};

Avant d'écrire le constructeur, examinons rapidement le code généré dans notre deuxième tutoriel sur le décodeur binaire. En plus de toutes les Extract, il y a la fonction DecodeRiscVInst32:

OpcodeEnum DecodeRiscVInst32(uint32_t inst_word);

Cette fonction prend l'instruction à décoder et renvoie le code d’opération qui correspond à cette instruction. D'un autre côté, La classe DecodeInterface qui RiscV32Decoder implémente uniquement les cartes dans une adresse e-mail. Ainsi, la classe RiscV32Decoder doit pouvoir accéder à la mémoire Lisez le mot d'instruction pour le transmettre à DecodeRiscVInst32(). Dans ce projet l'accès à la mémoire passe par une interface de mémoire simple définie dans .../mpact/sim/util/memory porte bien le nom util::MemoryInterface, vu ci-dessous:

  // Load data from address into the DataBuffer, then schedule the Instruction
  // inst (if not nullptr) to be executed (using the function delay line) with
  // context. The size of the data access is based on size of the data buffer.
  virtual void Load(uint64_t address, DataBuffer *db, Instruction *inst,
                    ReferenceCount *context) = 0;

De plus, nous devons être en mesure de transmettre une instance de classe state à la les constructeurs des autres classes de décodeur. La classe d'état appropriée est riscv::RiscVState, qui dérive de generic::ArchState, avec en plus pour RiscV. Cela signifie que nous devons déclarer le constructeur peut utiliser un pointeur vers state et memory:

RiscV32Decoder(riscv::RiscVState *state, util::MemoryInterface *memory);

Supprimez le constructeur par défaut et remplacez le destructeur:

RiscV32Decoder() = delete;
~RiscV32Decoder() override;

Déclarez ensuite la méthode DecodeInstruction à remplacer generic::DecoderInterface

generic::Instruction *DecodeInstruction(uint64_t address) override;

Si vous avez besoin d'aide (ou si vous souhaitez vérifier votre travail), la réponse complète est cliquez ici.


Définitions des membres des données

La classe RiscV32Decoder a besoin de membres de données privées pour stocker la les paramètres du constructeur et un pointeur vers la classe Factory.

 private:
  riscv::RiscVState *state_;
  util::MemoryInterface *memory_;

Il a également besoin d'un pointeur vers la classe d'encodage dérivée de RiscV32IEncodingBase, appelons cette méthode RiscV32IEncoding (nous implémenterons dans l'exercice 2). De plus, il a besoin d'un pointeur vers une instance de RiscV32IInstructionSet, ajoutez donc les éléments suivants:

  RiscV32IsaFactory *riscv_isa_factory_;
  RiscV32IEncoding *riscv_encoding_;
  RiscV32IInstructionSet *riscv_isa_;

Enfin, nous devons définir un membre de données à utiliser avec notre interface mémoire:

  generic::DataBuffer *inst_db_;

Si vous avez besoin d'aide (ou si vous souhaitez vérifier votre travail), la réponse complète est cliquez ici.

Définir les méthodes de la classe Decoder

Ensuite, il est temps d'implémenter le constructeur, le destructeur et le DecodeInstruction. Ouvrez le fichier riscv32_decoder.cc. Le vide déjà présentes dans le fichier, ainsi que les déclarations d'espace de noms et quelques sur using déclarations.

Constructeur – Définition

Le constructeur n'a besoin que d'initialiser les membres des données. Commencez par initialiser state_ et memory_:

RiscV32Decoder::RiscV32Decoder(riscv::RiscVState *state,
                               util::MemoryInterface *memory)
    : state_(state), memory_(memory) {

Allouez ensuite des instances de chacune des classes liées au décodeur, en transmettant les paramètres appropriés.

  // Allocate the isa factory class, the top level isa decoder instance, and
  // the encoding parser.
  riscv_isa_factory_ = new RiscV32IsaFactory();
  riscv_isa_ = new RiscV32IInstructionSet(state, riscv_isa_factory_);
  riscv_encoding_ = new RiscV32IEncoding(state);

Enfin, allouez l'instance DataBuffer. Il est alloué à l'aide d'une fabrique accessible via le membre state_. Nous allouons un tampon de données de taille un seul uint32_t, qui correspond à la taille du mot d'instruction.

  inst_db_ = state_->db_factory()->Allocate<uint32_t>(1);

Définition du destructeur

Le destructeur est simple : il suffit de libérer les objets alloués dans le constructeur. mais avec une seule touche. L'instance de tampon de données est comptée comme référence. Au lieu de cela, en appelant delete sur ce pointeur, nous DecRef() l'objet:

RiscV32Decoder::~RiscV32Decoder() {
  inst_db_->DecRef();
  delete riscv_isa_;
  delete riscv_isa_factory_;
  delete riscv_encoding_;
}

Définition de la méthode

Dans notre cas, l'implémentation de cette méthode est assez simple. Nous partirons du principe que l'adresse est correctement alignée et qu'aucune vérification d'erreur supplémentaire n'est effectuée obligatoire.

Tout d'abord, l'instruction doit être extraite de la mémoire interface et l'instance DataBuffer.

  memory_->Load(address, inst_db_, nullptr, nullptr);
  uint32_t iword = inst_db_->Get<uint32_t>(0);

Ensuite, nous appelons l'instance RiscVIEncoding pour analyser l'instruction, ce qui doit être fait avant d'appeler le décodeur ISA lui-même. Pour rappel, l'ISA le décodeur appelle directement l'instance RiscVIEncoding pour obtenir l'opération. et les opérandes spécifiés par le mot d'instruction. Nous n'avons pas encore implémenté cette fonctionnalité. , mais utilisons void ParseInstruction(uint32_t) comme méthode.

  riscv_encoding_->ParseInstruction(iword);

Enfin, nous appelons le décodeur ISA, en transmettant l'adresse et la classe Encoding.

  auto *instruction = riscv_isa_->Decode(address, riscv_encoding_);
  return instruction;

Si vous avez besoin d'aide (ou si vous souhaitez vérifier votre travail), la réponse complète est cliquez ici.


La classe d'encodage

La classe d'encodage implémente une interface utilisée par la classe du décodeur. pour obtenir le code d'opération d'instruction, ses opérandes source et de destination, et les opérandes de ressource. Ces objets dépendent tous des informations du binaire le décodeur, comme le code d'opération, les valeurs de champs spécifiques dans d'instruction, etc. Elle est séparée de la classe du décodeur sont compatibles avec différents schémas d'encodage. à l'avenir.

RiscV32IEncodingBase est une classe abstraite. L'ensemble des méthodes que nous devons implémentée dans notre classe dérivée est présentée ci-dessous.

class RiscV32IEncodingBase {
 public:
  virtual ~RiscV32IEncodingBase() = default;

  virtual OpcodeEnum GetOpcode(SlotEnum slot, int entry) = 0;

  virtual ResourceOperandInterface *
              GetSimpleResourceOperand(SlotEnum slot, int entry, OpcodeEnum opcode,
                                       SimpleResourceVector &resource_vec, int end) = 0;

  virtual ResourceOperandInterface *
              GetComplexResourceOperand(SlotEnum slot, int entry, OpcodeEnum opcode,
                                        ComplexResourceEnum resource_op,
                                        int begin, int end) = 0;

  virtual PredicateOperandInterface *
              GetPredicate(SlotEnum slot, int entry, OpcodeEnum opcode,
                           PredOpEnum pred_op) = 0;

  virtual SourceOperandInterface *
              GetSource(SlotEnum slot, int entry, OpcodeEnum opcode,
                        SourceOpEnum source_op, int source_no) = 0;

  virtual DestinationOperandInterface *
              GetDestination(SlotEnum slot, int entry, OpcodeEnum opcode,
                             DestOpEnum dest_op, int dest_no, int latency) = 0;

  virtual int GetLatency(SlotEnum slot, int entry, OpcodeEnum opcode,
                         DestOpEnum dest_op, int dest_no) = 0;
};

À première vue, cela semble un peu compliqué, surtout avec le nombre de mais pour une architecture simple comme RiscV, nous ignorons la plupart les paramètres, car leurs valeurs seront implicites.

Examinons chacune de ces méthodes l'une après l'autre.

OpcodeEnum GetOpcode(SlotEnum slot, int entry);

La méthode GetOpcode renvoie le membre OpcodeEnum pour le membre actuel l'instruction, identifiant son code d'opération. La classe OpcodeEnum est défini dans le fichier riscv32i_enums.h du décodeur généré. La méthode prend deux paramètres, qui peuvent tous deux être ignorés pour nos besoins. Le premier de correspond au type d'emplacement (une classe d'énumération également définie dans riscv32i_enums.h), qui, puisque RiscV ne comporte qu'un seul emplacement, n'a qu'une seule valeur possible: SlotEnum::kRiscv32 Le second est le numéro d'instance de l'emplacement il y a plusieurs instances de l'emplacement, ce qui peut se produire dans certains VLIW d'architectures).

ResourceOperandInterface *
    GetSimpleResourceOperand(SlotEnum slot, int entry, OpcodeEnum opcode,
                                     SimpleResourceVector &resource_vec, int end)

ResourceOperandInterface *
    GetComplexResourceOperand(SlotEnum slot, int entry, OpcodeEnum opcode,
                                      ComplexResourceEnum resource_op,
                                      int begin, int end);

Les deux méthodes suivantes sont utilisées pour modéliser les ressources matérielles dans le processeur afin d’améliorer la précision du cycle. Pour les exercices de ce tutoriel, nous n'utiliserons pas Dans l'implémentation, ils seront donc bouchonnés, renvoyant ainsi nullptr.

PredicateOperandInterface *
            GetPredicate(SlotEnum slot, int entry, OpcodeEnum opcode,
                         PredOpEnum pred_op);

SourceOperandInterface *
            GetSource(SlotEnum slot, int entry, OpcodeEnum opcode,
                      SourceOpEnum source_op, int source_no);

DestinationOperandInterface *
            GetDestination(SlotEnum slot, int entry, OpcodeEnum opcode,
                           DestOpEnum dest_op, int dest_no, int latency);

Ces trois méthodes renvoient des pointeurs vers des objets opérandes utilisés dans les fonctions sémantiques d'instructions pour accéder à la valeur d'une instruction l'opérande de prédicat, chacun des opérandes sources d'instructions, et écrivent de nouveaux aux opérandes de destination de l'instruction. Puisque RiscV n'utilise pas d'instruction, cette méthode ne doit renvoyer que nullptr.

Le modèle des paramètres est similaire pour toutes ces fonctions. Tout d'abord, comme GetOpcode, l'emplacement et l'entrée sont transmis. Ensuite, le code d’opération du pour laquelle l'opérande doit être créé. Il n'est utilisé que si le différents opérandes doivent renvoyer différents objets d'opérande pour le même opérande. ce qui n'est pas le cas pour ce simulateur RiscV.

Vient ensuite l'entrée d'énumération d'opérande "Prédicat, Source" et "Destination" identifie l'opérande à créer. Ceux-ci proviennent des trois OpEnums dans riscv32i_enums.h, comme illustré ci-dessous:

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

  enum class SourceOpEnum {
    kNone = 0,
    kBimm12 = 1,
    kCsr = 2,
    kImm12 = 3,
    kJimm20 = 4,
    kRs1 = 5,
    kRs2 = 6,
    kSimm12 = 7,
    kUimm20 = 8,
    kUimm5 = 9,
    kPastMaxValue = 10,
  };

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

Si vous regardez à nouveau riscv32.isa , vous remarquerez qu'ils correspondent aux ensembles de données source et de destination noms d'opérandes utilisés dans la déclaration de chaque instruction. En utilisant différentes noms d'opérandes représentant différents champs de bits et opérandes il facilite l'écriture de la classe d'encodage, car le membre enum est détermine le type exact d'opérande à renvoyer, et il n'est pas nécessaire de prendre en compte les valeurs des paramètres d'emplacement, d'entrée ou de code d'opération.

Enfin, pour les opérandes source et de destination, la position ordinale du l'opérande est transmis (là encore, nous pouvons l'ignorer), et pour la destination, opérande, la latence (en cycles) qui s'écoule entre le moment où l'instruction est émis, et le résultat de la destination est disponible pour les instructions suivantes. Dans notre simulateur, cette latence est égale à 0, ce qui signifie que l'instruction écrit le résultat immédiatement dans le registre.

int GetLatency(SlotEnum slot, int entry, OpcodeEnum opcode,
                         DestOpEnum dest_op, int dest_no);

La fonction finale est utilisée pour obtenir la latence d'une destination particulière opérande s'il a été spécifié comme * dans le fichier .isa. Ce n'est pas courant, et n'est pas utilisé pour ce simulateur RiscV. Notre implémentation de cette fonction renvoie simplement 0.


Définir la classe d'encodage

Fichier d'en-tête (.h)

Méthodes

Ouvrez le fichier riscv32i_encoding.h. Tous les fichiers d'inclusion nécessaires ont déjà été ajoutés et que les espaces de noms ont été configurés. Tout ajout de code est a suivi le commentaire // Exercise 2.

Commençons par définir une classe RiscV32IEncoding qui hérite de la classe via l'interface générée par Google.

class RiscV32IEncoding : public RiscV32IEncodingBase {
 public:

};

Ensuite, le constructeur doit utiliser un pointeur vers l'instance d'état, dans ce cas un pointeur vers riscv::RiscVState. Le destructeur par défaut doit être utilisé.

explicit RiscV32IEncoding(riscv::RiscVState *state);
~RiscV32IEncoding() override = default;

Avant d'ajouter toutes les méthodes d'interface, ajoutons la méthode appelée par RiscV32Decoder pour analyser l'instruction:

void ParseInstruction(uint32_t inst_word);

Ajoutons ensuite les méthodes qui présentent des remplacements simples en ignorant noms des paramètres qui ne sont pas utilisés:

// Trivial overrides.
ResourceOperandInterface *GetSimpleResourceOperand(SlotEnum, int, OpcodeEnum,
                                                   SimpleResourceVector &,
                                                   int) override {
  return nullptr;
}

ResourceOperandInterface *GetComplexResourceOperand(SlotEnum, int, OpcodeEnum,
                                                    ComplexResourceEnum ,
                                                    int, int) override {
  return nullptr;
}

PredicateOperandInterface *GetPredicate(SlotEnum, int, OpcodeEnum,
                                        PredOpEnum) override {
  return nullptr;
}

int GetLatency(SlotEnum, int, OpcodeEnum, DestOpEnum, int) override { return 0; }

Enfin, ajoutez les autres forçages de méthode de l'interface publique, mais avec les implémentations sont différées vers le fichier .cc.


OpcodeEnum GetOpcode(SlotEnum, int) override;

SourceOperandInterface *GetSource(SlotEnum , int, OpcodeEnum,
                                  SourceOpEnum source_op, int) override;

DestinationOperandInterface *GetDestination(SlotEnum, int, OpcodeEnum,
                                            DestOpEnum dest_op, int,
                                            int latency) override;

Afin de simplifier l'implémentation de chacune des méthodes getter d'opérande nous allons créer deux tableaux de callables (objets fonction) indexés par valeur numérique des membres SourceOpEnum et DestOpEnum respectivement. De cette façon, les corps de ces objets aux méthodes sont réduits à appeler objet fonction de la valeur d'énumération transmise et renvoyant son résultat .

Pour organiser l'initialisation de ces deux tableaux, nous définissons deux tableaux privés qui seront appelées à partir du constructeur comme suit:

 private:
  void InitializeSourceOperandGetters();
  void InitializeDestinationOperandGetters();

Membres des données

Les membres de données requis sont les suivants:

  • state_ pour contenir la valeur riscv::RiscVState *.
  • inst_word_ de type uint32_t, qui contient la valeur de la de l'instruction.
  • opcode_ pour contenir le code d'opération de l'instruction actuelle qui est mise à jour par la méthode ParseInstruction. Elle est de type OpcodeEnum.
  • source_op_getters_ : un tableau pour stocker les appelables utilisés pour obtenir la source opérandes. Le type des éléments du tableau est absl::AnyInvocable<SourceOperandInterface *>()>
  • dest_op_getters_ : un tableau pour stocker les appelables utilisés pour obtenir opérandes de destination. Le type des éléments du tableau est absl::AnyInvocable<DestinationOperandInterface *>()>
  • xreg_alias : tableau de noms d'ABI de registre d'entiers RiscV, par exemple "zéro" et "ra" au lieu de "x0" et "x1".

  riscv::RiscVState *state_;
  uint32_t inst_word_;
  OpcodeEnum opcode_;

  absl::AnyInvocable<SourceOperandInterface *()>
      source_op_getters_[static_cast<int>(SourceOpEnum::kPastMaxValue)];
  absl::AnyInvocable<DestinationOperandInterface *(int)>
      dest_op_getters_[static_cast<int>(DestOpEnum::kPastMaxValue)];

  const std::string xreg_alias_[32] = {
      "zero", "ra", "sp", "gp", "tp",  "t0",  "t1", "t2", "s0", "s1", "a0",
      "a1",   "a2", "a3", "a4", "a5",  "a6",  "a7", "s2", "s3", "s4", "s5",
      "s6",   "s7", "s8", "s9", "s10", "s11", "t3", "t4", "t5", "t6"};

Si vous avez besoin d'aide (ou si vous souhaitez vérifier votre travail), la réponse complète est cliquez ici.

Fichier source (.cc).

Ouvrez le fichier riscv32i_encoding.cc. Tous les fichiers d'inclusion nécessaires ont déjà été ajoutés et que les espaces de noms ont été configurés. Tout ajout de code est a suivi le commentaire // Exercise 2.

Fonctions de l'outil d'aide

Nous commencerons par écrire quelques fonctions d'assistance que nous utiliserons pour créer les opérandes de registre source et de destination. Celles-ci seront modélisées un type d'enregistrement et appelle l'objet RiscVState pour obtenir un handle vers enregistrer un objet, puis appeler une méthode de fabrique d'opérandes dans l'objet de registre.

Commençons par les assistants d'opérande de destination:

template <typename RegType>
inline DestinationOperandInterface *GetRegisterDestinationOp(
    RiscVState *state, const std::string &name, int latency) {
  auto *reg = state->GetRegister<RegType>(name).first;
  return reg->CreateDestinationOperand(latency);
}

template <typename RegType>
inline DestinationOperandInterface *GetRegisterDestinationOp(
    RiscVState *state, const std::string &name, int latency,
    const std::string &op_name) {
  auto *reg = state->GetRegister<RegType>(name).first;
  return reg->CreateDestinationOperand(latency, op_name);
}

Comme vous pouvez le voir, il existe deux fonctions d'assistance. La seconde prend une couche supplémentaire Paramètre op_name qui permet à l'opérande d'avoir un nom ou une chaîne différents que le registre sous-jacent.

De même pour les assistants d'opérande sources:

template <typename RegType>
inline SourceOperandInterface *GetRegisterSourceOp(RiscVState *state,
                                                   const std::string &reg_name) {
  auto *reg = state->GetRegister<RegType>(reg_name).first;
  auto *op = reg->CreateSourceOperand();
  return op;
}

template <typename RegType>
inline SourceOperandInterface *GetRegisterSourceOp(RiscVState *state,
                                                   const std::string &reg_name,
                                                   const std::string &op_name) {
  auto *reg = state->GetRegister<RegType>(reg_name).first;
  auto *op = reg->CreateSourceOperand(op_name);
  return op;
}

Fonctions de constructeur et d'interface

Le constructeur et les fonctions d'interface sont très simples. Constructeur appelle simplement les deux méthodes d'initialisation pour initialiser les tableaux appelables pour les getters d'opérande.

RiscV32IEncoding::RiscV32IEncoding(RiscVState *state) : state_(state) {
  InitializeSourceOperandGetters();
  InitializeDestinationOperandGetters();
}

ParseInstruction stocke le mot d'instruction, puis l'OPcode qu'il obtient en appelant le code généré par le décodeur binaire.

// Parse the instruction word to determine the opcode.
void RiscV32IEncoding::ParseInstruction(uint32_t inst_word) {
  inst_word_ = inst_word;
  opcode_ = mpact::sim::codelab::DecodeRiscVInst32(inst_word_);
}

Enfin, les getters d'opérande renvoient la valeur de la fonction getter qu'il appelle. en fonction de la recherche dans le tableau à l'aide de la valeur d'énumération de l'opérande source/destination.


DestinationOperandInterface *RiscV32IEncoding::GetDestination(
    SlotEnum, int, OpcodeEnum, DestOpEnum dest_op, int, int latency) {
  return dest_op_getters_[static_cast<int>(dest_op)](latency);
}

SourceOperandInterface *RiscV32IEncoding::GetSource(SlotEnum, int, OpcodeEnum,
                                                    SourceOpEnum source_op, int) {
  return source_op_getters_[static_cast<int>(source_op)]();
}

Méthodes d'initialisation de tableau

Comme vous l'avez peut-être deviné, la majeure partie du travail consiste à initialiser le getter des tableaux, mais ne vous inquiétez pas, vous utilisez un modèle facile à répéter. commencer par InitializeDestinationOpGetters(), car il n'existe deux opérandes de destination.

Rappelez la classe DestOpEnum générée à partir de riscv32i_enums.h:

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

Pour dest_op_getters_, nous devons initialiser quatre entrées, une pour kNone, kCsr, kNextPc et kRd. Pour plus de commodité, chaque entrée est initialisée avec une lambda, mais vous pouvez également utiliser n'importe quelle autre forme d'appelable. La signature du lambda est void(int latency).

Jusqu'à présent, nous n'avons pas beaucoup parlé des différents types de destinations d'opérandes définis dans MPACT-Sim. Pour cet exercice, nous n'utiliserons types: generic::RegisterDestinationOperand défini dans register.h, et generic::DevNullOperand définis dans devnull_operand.h. Les détails de ces opérandes ne sont pas vraiment importants pour le moment, sauf que les Le premier permet d'écrire dans les registres, tandis que le second ignore toutes les écritures.

La première entrée pour kNone est simple : il suffit de renvoyer une valeur nullptr et, éventuellement, consignez une erreur.

void RiscV32IEncoding::InitializeDestinationOperandGetters() {
  // Destination operand getters.
  dest_op_getters_[static_cast<int>(DestOpEnum::kNone)] = [](int) {
    return nullptr;
  };

Vient ensuite kCsr. Ici, nous allons dupliquer un peu. Hello World programme ne repose sur aucune mise à jour de requête de signature de certificat réelle, mais il existe du code récurrent exécuter les instructions CSR. La solution consiste à le simuler registre standard nommé "CSR" et canaliser toutes ces écritures.

  dest_op_getters_[static_cast<int>(DestOpEnum::kCsr)] = [this](int latency) {
    return GetRegisterDestinationOp<RV32Register>(state_, "CSR", latency);
  };

Vient ensuite kNextPc, qui fait référence au "PC" registre. Il est utilisé comme cible pour toutes les instructions relatives aux branches et aux sauts. Dans RiscVState, le nom est défini comme suit : kPcName

  dest_op_getters_[static_cast<int>(DestOpEnum::kNextPc)] = [this](int latency) {
    return GetRegisterDestinationOp<RV32Register>(state_, RiscVState::kPcName, latency);
  }

Enfin, il y a l'opérande de destination kRd. Dans riscv32i.isa, l'opérande. rd n'est utilisé que pour faire référence au registre d'entiers encodés dans "rd" champ du mot d'instruction, il n'y a donc pas d'ambiguïté à laquelle il se réfère. Il y n'est qu'une des complications. L'enregistrement de x0 (nom Abi zero) est câblé sur 0. Nous utilisons donc DevNullOperand pour ce registre.

Dans cet getter, nous allons d'abord extraire la valeur du champ rd à l'aide de la méthode Extract générée à partir du fichier .bin_fmt. Si la valeur est 0, nous renvoie une valeur « DevNull » opérande, sinon nous renvoyons l'opérande de registre correct, veillez à utiliser l'alias de registre approprié comme nom d'opérande.

  dest_op_getters_[static_cast<int>(DestOpEnum::kRd)] = [this](int latency) {
    // First extract register number from rd field.
    int num = inst32_format::ExtractRd(inst_word_);
    // For register x0, return the DevNull operand.
    if (num == 0) return new DevNullOperand<uint32_t>(state, {1});
    // Return the proper register operand.
    return GetRegisterDestinationOp<RV32Register>(
      state_, absl::StrCat(RiscVState::kXRegPrefix, num), latency,
      xreg_alias_[num]);
    )
  }
}

Passons maintenant à la méthode InitializeSourceOperandGetters(), où le modèle est à peu près les mêmes, mais les détails diffèrent légèrement.

Commençons par examiner le SourceOpEnum généré à partir de riscv32i.isa dans le premier tutoriel:

  enum class SourceOpEnum {
    kNone = 0,
    kBimm12 = 1,
    kCsr = 2,
    kImm12 = 3,
    kJimm20 = 4,
    kRs1 = 5,
    kRs2 = 6,
    kSimm12 = 7,
    kUimm20 = 8,
    kUimm5 = 9,
    kPastMaxValue = 10,
  };

À l'examen, les membres se répartissent en deux groupes en plus de kNone. Un correspond aux opérandes immédiats: kBimm12, kImm12, kJimm20, kSimm12, kUimm20, et kUimm5. Les autres sont des opérandes de registre: kCsr, kRs1 et kRs2.

L'opérande kNone est géré comme pour les opérandes de destination : renvoie une "nullptr".

void RiscV32IEncoding::InitializeSourceOperandGetters() {
  // Source operand getters.
  source_op_getters_[static_cast<int>(SourceOpEnum::kNone)] = [] () {
    return nullptr;
  };

Passons maintenant aux opérandes de registre. Nous traiterons les kCsr similaires à la façon dont nous avons géré les opérandes de destination correspondants. Il suffit d'appeler la méthode la fonction d'assistance "CSR" comme nom d'enregistrement.

  // Register operands.
  source_op_getters_[static_cast<int>(SourceOpEnum::kCsr)] = [this]() {
    return GetRegisterSourceOp<RV32Register>(state_, "CSR");
  };

Les opérandes kRs1 et kRs2 sont gérés de la même manière que kRd, sauf que alors que nous ne voulions pas mettre à jour x0 (ou zero), nous voulons nous lisons toujours 0 à partir de cet opérande. Pour cela, nous allons utiliser Classe generic::IntLiteralOperand<> définie dans literal_operand.h Cet opérande permet de stocker une valeur littérale (par opposition à une simulation valeur immédiate). Sinon, le schéma est identique: commencez par extraire la valeur rs1/rs2 du mot d'instruction. Si elle est égale à zéro, renvoyez le littéral opérande avec un paramètre de modèle 0. Sinon, renvoie un registre standard Opérande source à l'aide de la fonction d'assistance, en utilisant l'alias "abi" comme opérande son nom.

  source_op_getters_[static_cast<int>(SourceOpEnum::kRs1)] =
      [this]() -> SourceOperandInterface * {
    int num = inst32_format::ExtractRs1(inst_word_);
    if (num == 0) return new IntLiteralOperand<0>({1}, xreg_alias_[0]);
    return GetRegisterSourceOp<RV32Register>(
        state_, absl::StrCat(RiscVState::kXregPrefix, num), xreg_alias_[num]);
  };
  source_op_getters_[static_cast<int>(SourceOpEnum::kRs2)] =
      [this]() -> SourceOperandInterface * {
    int num = inst32_format::ExtractRs2(inst_word_);
    if (num == 0) return new IntLiteralOperand<0>({1}, xreg_alias_[0]);
    return GetRegisterSourceOp<RV32Register>(
        state_, absl::StrCat(RiscVState::kXregPrefix, num), xreg_alias_[num]);
  };

Enfin, nous traitons les différents opérandes immédiats. Les valeurs immédiates sont stocké dans des instances de la classe generic::ImmediateOperand<> définie dans immediate_operand.h La seule différence entre les différents getters pour les opérandes immédiats la fonction Extractor utilisée, et si le type de stockage est non signé, selon le champ de bits.

  // Immediates.
  source_op_getters_[static_cast<int>(SourceOpEnum::kBimm12)] = [this]() {
    return new ImmediateOperand<int32_t>(
        inst32_format::ExtractBImm(inst_word_));
  };
  source_op_getters_[static_cast<int>(SourceOpEnum::kImm12)] = [this]() {
    return new ImmediateOperand<int32_t>(
        inst32_format::ExtractImm12(inst_word_));
  };
  source_op_getters_[static_cast<int>(SourceOpEnum::kUimm5)] = [this]() {
    return new ImmediateOperand<uint32_t>(
        inst32_format::ExtractUimm5(inst_word_));
  };
  source_op_getters_[static_cast<int>(SourceOpEnum::kJimm20)] = [this]() {
    return new ImmediateOperand<int32_t>(
        inst32_format::ExtractJImm(inst_word_));
  };
  source_op_getters_[static_cast<int>(SourceOpEnum::kSimm12)] = [this]() {
    return new ImmediateOperand<int32_t>(
        inst32_format::ExtractSImm(inst_word_));
  };
  source_op_getters_[static_cast<int>(SourceOpEnum::kUimm20)] = [this]() {
    return new ImmediateOperand<uint32_t>(
        inst32_format::ExtractUimm32(inst_word_));
  };
}

Si vous avez besoin d'aide (ou si vous souhaitez vérifier votre travail), la réponse complète est cliquez ici.

Ce tutoriel est maintenant terminé. Nous espérons qu'elle vous a été utile.