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 valorriscv::RiscVState *
.inst_word_
de tipouint32_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étodoParseInstruction
Esto tiene el tipoOpcodeEnum
.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 esabsl::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 esabsl::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 ®_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 ®_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.