Decodificador integrado de RiscV

Los objetivos de este instructivo son los siguientes:

  • Aprende cómo se ajustan los decodificadores binarios y los ISA generados.
  • Escribe el código C++ necesario para crear un decodificador de instrucciones completo para RiscV RV32I que combina ISA y decodificadores binarios.

Comprender el decodificador de instrucciones

El decodificador de instrucciones es responsable de, según una dirección de instrucciones, leer la palabra de instrucción de la memoria y devolver una instancia completamente inicializada del Instruction que representa esa instrucción.

El decodificador de nivel superior implementa el generic::DecoderInterface que se muestra a continuación:

// 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;
};

Como puedes ver, solo se debe implementar un método: cpp virtual Instruction *DecodeInstruction(uint64_t address);.

Ahora, veamos lo que se proporciona y lo que necesita el código generado.

Primero, considera la clase de nivel superior RiscV32IInstructionSet del archivo. riscv32i_decoder.h, que se generó al final del instructivo en el decodificador ISA. Para ver el contenido de nuevo, ve al directorio de soluciones de ese instructivo y volver a compilar todo.

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

Ahora, cambia tu directorio nuevamente a la raíz del repositorio. Luego, veamos en las fuentes que se generaron. Para eso, cambia el directorio bazel-out/k8-fastbuild/bin/riscv_isa_decoder (suponiendo que usas un x86 host: para otros hosts, k8-fastbuild será otra cadena).

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

Verás los cuatro archivos fuente que contienen el código C++ generado en la lista:

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

Abre el primer archivo riscv32i_decoder.h. Hay tres clases que debemos analizar lo siguiente:

  • RiscV32IEncodingBase
  • RiscV32IInstructionSetFactory
  • RiscV32IInstructionSet

Ten en cuenta los nombres de las clases. Todas las clases se nombran según el Versión en mayúsculas y minúsculas del nombre proporcionado en la palabra “isa” en ese archivo: isa RiscV32I { ... }

Comencemos con la clase RiscVIInstructionSet. Se muestra a continuación:

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_;
};

No hay métodos virtuales en esta clase, por lo que esta es una clase independiente, pero nota dos cosas. Primero, el constructor apunta a una instancia de la Clase RiscV32IInstructionSetFactory. Esta es una clase que el sistema de decodificador usa para crear una instancia de la clase RiscV32Slot, que se usa para y decodificar todas las instrucciones definidas para slot RiscV32 como se definen en Archivo riscv32i.isa. En segundo lugar, el método Decode toma un parámetro adicional de tipo puntero a RiscV32IEncodingBase, esta es una clase que proporcionará el entre el decodificador isa generado en el primer instructivo y el objeto binario decodificador generado en el segundo lab.

La clase RiscV32IInstructionSetFactory es una clase abstracta a partir de la cual se nuestra propia implementación para el decodificador completo. En la mayoría de los casos, esto es trivial: solo debes proporcionar un método para llamar al constructor para cada definida en nuestro archivo .isa. En nuestro caso, es muy simple es solo una de esas clases: Riscv32Slot (mayúsculas y minúsculas del nombre riscv32). se concatena con Slot). El método no se genera porque hay algunos casos de uso avanzados en los que podría ser útil derivar una subclase de la ranura y, en su lugar, llama a su constructor.

Pasaremos por la clase final RiscV32IEncodingBase más adelante. en este instructivo, ya que ese es el tema de otro ejercicio.


Definir el decodificador de instrucciones de nivel superior

Define la clase de fábrica

Si volviste a compilar el proyecto para el primer instructivo, asegúrate de volver a el directorio riscv_full_decoder.

Abre el archivo riscv32_decoder.h. Todos los archivos de inclusión necesarios tienen ya se agregó y se configuraron los espacios de nombres.

Después de marcar el comentario como //Exercise 1 - step 1, define la clase Se hereda RiscV32IsaFactory de RiscV32IInstructionSetFactory.

class RiscV32IsaFactory : public RiscV32InstructionSetFactory {};

A continuación, define la anulación para CreateRiscv32Slot. Como no usamos clases derivadas de Riscv32Slot, simplemente asignamos una nueva instancia usando std::make_unique

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

Si necesitas ayuda (o quieres revisar tu trabajo), la respuesta completa es aquí.

Define la clase de decodificador

Declaraciones de constructores, destructores y métodos

A continuación, es momento de definir la clase de decodificador. En el mismo archivo que arriba, ve a la la declaración de RiscV32Decoder. Expande la declaración en una definición de clase En el ejemplo anterior, RiscV32Decoder se hereda de generic::DecoderInterface.

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

Antes de escribir el constructor, repasemos el código. generado en nuestro segundo instructivo sobre el decodificador binario. Además de todas las Extract, existe la función DecodeRiscVInst32:

OpcodeEnum DecodeRiscVInst32(uint32_t inst_word);

Esta función toma la palabra de instrucción que se debe decodificar y muestra el código de operación que coincida con esa instrucción. Por otro lado, La clase DecodeInterface que RiscV32Decoder implementa solo pasa en un web. Por lo tanto, la clase RiscV32Decoder debe poder acceder a la memoria para lo siguiente: leerá la palabra de la instrucción que se pasará a DecodeRiscVInst32(). En este proyecto La forma de acceder a la memoria es a través de una interfaz de memoria simple definida en .../mpact/sim/util/memory llamado util::MemoryInterface, como se muestra a continuación:

  // 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;

Además, debemos poder pasar una instancia de clase state al archivo constructores de las otras clases de decodificador. La clase de estado apropiada es clase riscv::RiscVState, que deriva de generic::ArchState, con para RiscV. Esto significa que debemos declarar el constructor para que puede llevar un puntero a state y a memory:

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

Borra el constructor predeterminado y anula el destructor:

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

Luego, declara el método DecodeInstruction que debemos anular generic::DecoderInterface

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

Si necesitas ayuda (o quieres revisar tu trabajo), la respuesta completa es aquí.


Definiciones de los miembros de datos

La clase RiscV32Decoder necesitará miembros de datos privados para almacenar la parámetros de constructor y un puntero a la clase de fábrica.

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

También necesita un puntero a la clase de codificación que se deriva de RiscV32IEncodingBase, llamaremos RiscV32IEncoding (implementaremos esto en el ejercicio 2). Además, necesita un puntero hacia una instancia de RiscV32IInstructionSet, así que agrega lo siguiente:

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

Por último, debemos definir un miembro de datos para usar con nuestra interfaz de memoria:

  generic::DataBuffer *inst_db_;

Si necesitas ayuda (o quieres revisar tu trabajo), la respuesta completa es aquí.

Definición de los métodos de clase de decodificador

Luego, es momento de implementar el constructor, el destructor y el DecodeInstruction. Abre el archivo riscv32_decoder.cc. El vacío ya están en el archivo, así como las declaraciones de espacio de nombres y un par de using declaraciones.

Definición del constructor

El constructor solo necesita inicializar los miembros de datos. Primero, inicializa el state_ y memory_:

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

Luego, asigna instancias de cada una de las clases relacionadas con el decodificador y pasa el parámetros adecuados.

  // 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);

Por último, asigna la instancia DataBuffer. Se asigna mediante una configuración accesible a través del miembro state_. Asignamos un búfer de datos del tamaño para almacenar un solo elemento uint32_t, ya que ese es el tamaño de la palabra de instrucción.

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

Definición de Destructor

El destructor es simple, solo libera los objetos que asignamos en el constructor, pero con un giro. La instancia de búfer de datos se cuenta por referencia, por lo que Si llamamos a delete en ese puntero, aplicaremos DecRef() al objeto:

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

Definición del método

En nuestro caso, la implementación de este método es bastante simple. Supongamos que la dirección esté alineada correctamente y que no se produzcan verificaciones de errores adicionales como en los productos necesarios.

Primero, la palabra de la instrucción se debe recuperar de la memoria usando el método y la instancia DataBuffer.

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

A continuación, llamamos a la instancia RiscVIEncoding para analizar la palabra de instrucción. lo que debe hacerse antes de llamar al mismo decodificador ISA. Recuerda que el ISA el decodificador llama a la instancia RiscVIEncoding directamente para obtener el código de operación y operandos especificados por la palabra de instrucción. No lo implementamos pero usemos void ParseInstruction(uint32_t) como ese método.

  riscv_encoding_->ParseInstruction(iword);

Por último, llamamos al decodificador ISA, pasando la dirección y la clase de codificación.

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

Si necesitas ayuda (o quieres revisar tu trabajo), la respuesta completa es aquí.


La clase de codificación

La clase de codificación implementa una interfaz utilizada por la clase de decodificador. para obtener el código de operación de la instrucción, sus operandos de origen y destino, y operandos de recursos. Todos estos objetos dependen de la información del objeto binario de formato, como el código de operación, los valores de campos específicos en la palabra de instrucción, etc. Esto está separado de la clase de codificador para mantenerlo Es independiente de la codificación y admiten la compatibilidad con varios esquemas de codificación diferentes. en el futuro.

RiscV32IEncodingBase es una clase abstracta. El conjunto de métodos que tenemos para implementar en nuestra clase derivada se muestra a continuación.

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;
};

A primera vista, parece un poco complicado, sobre todo por la cantidad de parámetros, pero para una arquitectura simple como RiscV en realidad ignoramos la mayor parte de los parámetros, ya que sus valores serán implícitos.

Veamos cada uno de los métodos a la vez.

OpcodeEnum GetOpcode(SlotEnum slot, int entry);

El método GetOpcode devuelve el miembro OpcodeEnum para el instrucción, lo que identifica el código de operación de la instrucción. La clase OpcodeEnum es definido en el archivo de decodificador isa riscv32i_enums.h generado. El método toma dos parámetros, los cuales pueden ignorarse para nuestros fines. El primero de este es el tipo de ranura (una clase enum que también se define en riscv32i_enums.h) que, como RiscV tiene una sola ranura, solo tiene un valor posible: SlotEnum::kRiscv32 El segundo es el número de instancia de la ranura (en caso hay varias instancias de la ranura, lo que puede ocurrir en algunos VLIW arquitecturas).

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);

Los siguientes dos métodos se usan para modelar recursos de hardware en el procesador para mejorar la precisión del ciclo. Para nuestros ejercicios de tutoriales, no usaremos por lo que, en la implementación, se hará un stub y mostrará 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);

Estos tres métodos devuelven punteros a los objetos de operando que se usan las funciones semánticas de instrucción para acceder al valor de cualquier instrucción operando de predicado, cada uno de los operandos de origen de la instrucción, y escribir nuevos valores a los operandos de destino de la instrucción. Como RiscV no usa predicados de instrucción, ese método solo necesita mostrar nullptr.

El patrón de parámetros es similar entre estas funciones. Primero, tal como GetOpcode: Se pasa el espacio y la entrada. Luego, el código de operación de la instrucción para la que se debe crear el operando. Esto solo se usa si el diferentes códigos de operación deben devolver diferentes objetos de operando para el mismo operando que no es el caso de este simulador de RiscV.

A continuación, se encuentra la entrada de enumeración de operando, predicado, fuente y destino, que identifica el operando que se debe crear. Estas provienen de las tres OpEnums en riscv32i_enums.h, como se muestra a continuación:

  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 piensas en los riscv32.isa notarás que estos corresponden a los conjuntos de orígenes y nombres de operandos usados en la declaración de cada instrucción. Usando diferentes Nombres de operandos para operandos que representan diferentes campos de bits y operandos , facilita la escritura de la clase de codificación, ya que el miembro enum es de forma única determina el tipo de operando exacto que se devolverá, y no es necesario para tener en cuenta los valores de los parámetros de espacio, entrada o código de operación.

Finalmente, para los operandos de origen y destino, la posición ordinal del se pasa el operando (nuevamente, podemos ignorarlo), y para el destino operando, la latencia (en ciclos) que transcurre entre el tiempo en que la instrucción se emite y el resultado de destino está disponible para instrucciones posteriores. En nuestro simulador, esta latencia será 0, lo que significa que la instrucción escribe el resultado de inmediato al registro.

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

La función final se usa para obtener la latencia de un destino en particular. operando si se especificó como * en el archivo .isa. Esto es poco común, y no se usa para este simulador de RiscV, por lo que la implementación de esta función devolverá 0.


Define la clase de codificación

Archivo de encabezado (.h)

Métodos

Abre el archivo riscv32i_encoding.h. Todos los archivos de inclusión necesarios tienen ya se agregó y se configuraron los espacios de nombres. Toda la adición de código se has seguido el comentario // Exercise 2.

Comencemos por definir una clase RiscV32IEncoding que se herede de la interfaz generada.

class RiscV32IEncoding : public RiscV32IEncodingBase {
 public:

};

A continuación, el constructor debería apuntar a la instancia de estado, en este caso. un puntero a riscv::RiscVState. Se debería usar el destructor predeterminado.

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

Antes de agregar todos los métodos de la interfaz, agreguemos el método que llama RiscV32Decoder para analizar la instrucción:

void ParseInstruction(uint32_t inst_word);

A continuación, agreguemos aquellos métodos que tienen anulaciones triviales mientras descartamos los nombres de los parámetros que no se usan:

// 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; }

Por último, agrega las anulaciones de método restantes de la interfaz pública, pero con las implementaciones aplazadas al archivo .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;

Para simplificar la implementación de cada uno de los métodos get de operando Crearemos dos arrays de callables (objetos de función) indexadas por el El valor numérico de los miembros SourceOpEnum y DestOpEnum, respectivamente. De esta manera, los cuerpos de estos a métodos se reducen a llamar objeto de la función para el valor enum que se pasa y muestra su resultado valor.

Para organizar la inicialización de estos dos arrays, definimos dos arrays métodos que se llamarán desde el constructor de la siguiente manera:

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

Miembros de datos

Los miembros de datos requeridos son los siguientes:

  • state_ para contener el valor riscv::RiscVState *.
  • inst_word_ de tipo uint32_t, que contiene el valor del valor actual palabra de instrucción.
  • opcode_ para contener el código de operación de la instrucción actual que actualiza el método ParseInstruction Esto tiene el tipo OpcodeEnum.
  • source_op_getters_: Es un array para almacenar las llamadas que se usan para obtener la fuente. objetos de operando. El tipo de elementos del array es absl::AnyInvocable<SourceOperandInterface *>()>
  • dest_op_getters_: Es un array para almacenar las llamadas que se usan para obtener. objetos de operando de destino. El tipo de elementos del array es absl::AnyInvocable<DestinationOperandInterface *>()>
  • xreg_alias: Es un array de nombres de ABI de registro de números enteros RiscV, p.ej., “cero” y “ra” en vez de "x0" y "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 necesitas ayuda (o quieres revisar tu trabajo), la respuesta completa es aquí.

Archivo de origen (.cc).

Abre el archivo riscv32i_encoding.cc. Todos los archivos de inclusión necesarios tienen ya se agregó y se configuraron los espacios de nombres. Toda la adición de código se has seguido el comentario // Exercise 2.

Funciones auxiliares

Comenzaremos por escribir algunas funciones auxiliares que usamos para crear operandos de registro de origen y destino. Estas se crearán en plantillas de registro y llamará al objeto RiscVState para controlar el registrar objeto y, luego, llama a un método de fábrica de operando en el objeto de registro.

Comencemos con los asistentes del operando de destino:

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);
}

Como puedes ver, hay dos funciones auxiliares. El segundo requiere parámetro op_name que permite que el operando tenga un nombre o una cadena diferentes que el registro subyacente.

De manera similar para los asistentes del operando de origen:

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;
}

Funciones de interfaz y constructor

El constructor y las funciones de la interfaz son muy simples. El constructor solo llama a los dos métodos de inicialización para inicializar los arrays de callables para los métodos get de operando.

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

ParseInstruction almacena la palabra de instrucción y, luego, el código de operación que que obtiene llamando al código generado por el decodificador binario.

// 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_);
}

Por último, los métodos get de operando muestran el valor de la función get que llama en función de la búsqueda de arrays con el valor de enumeración del operando destino/origen.


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étodos de inicialización de array

Como habrás adivinado, la mayor parte del trabajo consiste en inicializar el método get. pero no te preocupes, esto se hace usando un patrón fácil y repetitivo. Vamos a comienza con InitializeDestinationOpGetters(), ya que solo hay un un par de operandos de destino.

Recupera la clase DestOpEnum generada desde riscv32i_enums.h:

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

Para dest_op_getters_, debemos inicializar 4 entradas, una para cada kNone. kCsr, kNextPc y kRd. Para mayor comodidad, cada entrada se inicializa con un lambda, aunque también podrías usar cualquier otro tipo de función que admita llamadas. La firma de la lambda es void(int latency).

Hasta ahora, no hemos hablado mucho sobre los diferentes tipos de destinos operandos definidos en MPACT-Sim. Para este ejercicio, solo usaremos dos tipos: generic::RegisterDestinationOperand definidos en register.h, y generic::DevNullOperand definidos en devnull_operand.h. Los detalles de estos operandos no son muy importantes en este momento, excepto que los el primero se usa para escribir en registros y el segundo ignora todas las escrituras.

La primera entrada para kNone es trivial: solo muestra un nullptr y, de manera opcional, registrar un error.

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

La siguiente es kCsr. Aquí vamos a hacer trampa un poco. Hello World programa no depende de ninguna actualización real de CSR, pero hay código estándar que ejecutar instrucciones de CSR. La solución es hacer una simulación con registro regular llamado “CSR” y canalizar todas esas escrituras en ella.

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

La siguiente es kNextPc, que se refiere a la "pc" de registro. Se usa como destino para todas las instrucciones de rama y salto. El nombre se define en RiscVState de la siguiente manera: kPcName

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

Por último, está el operando de destino kRd. En riscv32i.isa, el operando rd solo se usa para hacer referencia al registro de números enteros codificado en "rd" campo de la palabra de instrucción, por lo que no hay ambigüedad a la que se refiere. Hay es solo una complicación. El registro x0 (nombre de ABI zero) está conectado por cable a 0, así que, para ese registro, usamos DevNullOperand.

En este método get, primero extraemos el valor en el campo rd con el método Extract generado a partir del archivo .bin_fmt. Si el valor es 0, devuelve un valor "DevNull" operando. De lo contrario, devolvemos el operando de registro correcto, y asegúrate de usar el alias de registro apropiado como nombre del operando.

  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]);
    )
  }
}

Ahora, volvamos al método InitializeSourceOperandGetters(), donde el patrón es prácticamente igual, pero los detalles difieren un poco.

Primero, veamos el objeto SourceOpEnum que se generó a partir de riscv32i.isa en el primer instructivo:

  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,
  };

Examinamos a los miembros, además de kNone, se clasifican en dos grupos. Uno son operandos inmediatos: kBimm12, kImm12, kJimm20, kSimm12, kUimm20, y kUimm5. Los otros son operandos de registro: kCsr, kRs1 y kRs2.

El operando kNone se controla igual que los operandos de destino: muestra un nullptr.

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

A continuación, trabajemos en los operandos de registro. Manejaremos kCsr de forma similar a cómo manejamos los operandos de destino correspondientes; simplemente llama al función auxiliar con “CSR” como el nombre del registro.

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

Los operandos kRs1 y kRs2 se controlan de forma equivalente a kRd, con la excepción de que Si bien no queremos actualizar x0 (ni zero), queremos asegurarnos de que siempre leemos 0 de ese operando. Para eso, usaremos la Clase generic::IntLiteralOperand<> definida en literal_operand.h Este operando se usa para almacenar un valor literal (en contraposición a un valor simulado valor inmediato). De lo contrario, el patrón es el mismo: primero, extrae El valor rs1/rs2 de la palabra de instrucción; si es cero, muestra el valor literal operando con un parámetro de plantilla 0; de lo contrario, devuelve un registro regular operando de origen con la función auxiliar, usando el alias de abi como el operando de la fuente de datos.

  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]);
  };

Finalmente, manejamos los diferentes operandos inmediatos. Los valores inmediatos son almacenados en instancias de la clase generic::ImmediateOperand<> definida en immediate_operand.h La única diferencia entre los diferentes métodos get para los operandos inmediatos es qué función de extractor se usa y si el tipo de almacenamiento está firmado o sin signo, según el campo 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 necesitas ayuda (o quieres revisar tu trabajo), la respuesta completa es aquí.

Con esto concluye este instructivo. Esperamos que te haya resultado útil.