Decodificador ISA RiscV

Los objetivos de este instructivo son los siguientes:

  • Descubre cómo se representan las instrucciones en el simulador de MPACT-Sim.
  • Obtén información sobre la estructura y la sintaxis del archivo de descripción de ISA.
  • Escribir las descripciones de ISA para el subconjunto de instrucciones de RiscV RV32I

Descripción general

En MPACT-Sim, las instrucciones de destino se decodifican y almacenan en una para que la información esté más disponible y la semántica más rápido para ejecutarse. Estas instancias de instrucciones se almacenan en caché para reducir la cantidad de veces que se ejecutan instrucciones ejecutado.

La clase de instrucción

Antes de comenzar, es útil observar un poco cómo se diseñan las instrucciones representadas en MPACT-Sim. La clase Instruction se define en mpact-sim/mpact/sim/generic/instruction.h.

La instancia de clase Instruction contiene toda la información necesaria para simular la instrucción cuando se "ejecuta", por ejemplo:

  1. Dirección de la instrucción, tamaño de la instrucción simulada, es decir, tamaño en .text.
  2. Código de operación de la instrucción.
  3. Puntero de la interfaz de operando del predicado (si corresponde).
  4. Vector de los punteros de la interfaz de operando de origen.
  5. Vector de punteros de la interfaz de operando de destino.
  6. Función semántica que admite llamadas.
  7. Es el puntero al objeto de estado de la arquitectura.
  8. Es el puntero al objeto de contexto.
  9. Es el puntero a las instancias de instrucciones secundarias y siguientes.
  10. Cadena de desensamblado.

Por lo general, estas instancias se almacenan en una caché de instrucciones (instancia). y se reutiliza cada vez que se vuelve a ejecutar la instrucción. Esto mejora el rendimiento durante el tiempo de ejecución.

Excepto por el puntero hacia el objeto de contexto, todos se completan con el de instrucciones que se genera a partir de la descripción de ISA. Para este no es necesario conocer los detalles de estos elementos, ya que no la las usan directamente. Por el contrario, se puede obtener un nivel de comprensión alto sobre cómo se usan suficientes.

La función semántica que admite llamadas es el objeto de función/método/función de C++ (incluidas las lambdas) que implementa la semántica de la instrucción. Para para una instrucción add, carga cada operando de origen, agrega los dos operandos y escribe el resultado en un solo operando de destino. El tema de funciones semánticas se tratan en profundidad en el instructivo de funciones semánticas.

Operandos de instrucción

La clase de instrucciones incluye punteros a tres tipos de interfaces de operando: predicado, origen y destino. Estas interfaces permiten que las funciones semánticas escribirse independientemente del tipo real de la instrucción subyacente operando. Por ejemplo, se puede acceder a los valores de los registros y los inmediatos a través de la misma interfaz. Esto significa que las instrucciones que realizan el mismo pero en diferentes operandos (p. ej., registros vs. imágenes inmediatas) se puede implementado usando la misma función semántica.

La interfaz de operando de predicado, para los ISA que admiten valores de ejecución de instrucciones (para otros ISA, es nula), se usa para determinar si un instrucción dada debe ejecutarse según el valor booleano del predicado.

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

La interfaz de operando de origen permite que la función semántica de instrucción lea valores de los operandos de las instrucciones sin importar el operando subyacente el tipo de letra. Los métodos de interfaz admiten operandos escalares y con valor vectorial.

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

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

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

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

  virtual ~SourceOperandInterface() = default;
};

La interfaz de operando de destino proporciona métodos para la asignación y el manejo Instancias de DataBuffer (el tipo de datos interno que se usa para almacenar valores de registros). R el operando de destino también tiene una latencia asociada, que es el número de ciclos que esperar hasta que se cree la instancia de búfer de datos asignada por la instrucción función semántica se usa para actualizar el valor del registro de destino. Para instancia, la latencia de una instrucción add puede ser 1, mientras que, para una instrucción mpy, 4. Esto se aborda con más detalle en el instructivo sobre funciones semánticas.

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

Descripción de ISA

El ISA (arquitectura de conjunto de instrucciones) de un procesador define el modelo abstracto a través del cual el software interactúa con el hardware. Define el conjunto de las instrucciones disponibles, los tipos de datos, los registros y otros estados de las máquinas en las que operan las instrucciones, así como su comportamiento (semántica). Para los fines de MPACT-Sim, el ISA no incluye la codificación real de las instrucciones. Eso se trata por separado.

El ISA del procesador se expresa en un archivo de descripción que describe el conjunto de instrucciones a un nivel abstracto y agnóstico de la codificación. El archivo de descripción enumera el conjunto de instrucciones disponibles. Para cada instrucción es obligatorio incluir su nombre, el número y los nombres de sus operandos, y su vinculación a una función o función de C++ que admite llamadas que implementa su semántica. Además: se puede especificar una cadena de formato de desensamblado, y el uso de la instrucción los nombres de recursos de hardware. El primero es útil para producir un texto representación de la instrucción para la depuración, el seguimiento o el uso interactivo. El esta última se puede usar para lograr una mayor exactitud de los ciclos en la simulación.

El archivo de descripción de ISA es analizado por el isa-parser que genera el código para el decodificador de instrucciones independiente de la representación. Este decodificador es responsable de para completar los campos de los objetos de instrucción. Los valores específicos, por ejemplo, número de registro de destino, se obtienen de una instrucción específica de formato decodificador. Uno de estos decodificadores es el decodificador binario, que es el foco del en el próximo instructivo.

En este instructivo, se explica cómo escribir un archivo de descripción de ISA para una estructura escalar simple arquitectura. Usaremos un subconjunto de instrucciones RiscV RV32I configurado para ilustrar esto y, junto con los otros tutoriales, crear un simulador capaz de simular la frase “Hello World” . Para obtener más detalles sobre el ISA de RiscV, consulta Especificaciones de Risc-V.

Para comenzar, abre el archivo: riscv_isa_decoder/riscv32i.isa

El contenido del archivo se divide en varias secciones. Primero, está el ISA declarativa:

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

De esta manera, se declara que RiscV32I es el nombre del ISA, y el generador de código crea una clase llamada RiscV32IEncodingBase que defina la interfaz de decodificador generado para obtener información del código de operación y el operando. Nombre de Esta clase se genera convirtiendo el nombre del ISA a mayúscula inicial (pascal case), luego concatenándola con EncodingBase. La declaración slots { riscv32; } especifica que solo hay una única ranura de instrucción riscv32 en el RiscV32I en una instrucción de VLIW y que la única Las instrucciones válidas son aquellas definidas para ejecutarse en riscv32.

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

Esto especifica que el primer fragmento de desensamblado de cualquier especificación (ver más abajo), se justificará a la izquierda en 15 caracteres un campo amplio. Cualquier fragmento posterior se agregará a este campo sin cualquier espaciado adicional.

Debajo, hay tres declaraciones de ranuras: riscv32i, zicsr y riscv32. Según la definición de isa anterior, solo las instrucciones definidas para riscv32 el espacio disponible será parte del isa RiscV32I. ¿Para qué sirven los otros dos horarios disponibles?

Los espacios se pueden usar para factorizar instrucciones en grupos separados en una sola ranura al final. Observa la notación : riscv32i, zicsr en la declaración de ranuras riscv32. Este comando especifica que la ranura riscv32 hereda todas las instrucciones definidas en los espacios zicsr y riscv32i. El ISA de 32 bits de RiscV consta de un ISA base llamado RV32I, al que se puede enviar que se agregará. El mecanismo de ranura permite que las instrucciones de estas extensiones se especifican por separado y, luego, se combinan, según sea necesario, para definir los ISA general. En este caso, las instrucciones en el encabezado RiscV “I” grupo están definidos separados de los que se encuentran en "zicsr" grupo. Se pueden definir grupos adicionales. de la 'M' (multiplicar/dividir), 'F' (punto flotante con precisión sencilla), 'D' (punto flotante de doble precisión), 'C' (instrucciones compactas de 16 bits), etc. necesarios para obtener el ISA de RiscV final deseado.

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

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

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

No es necesario cambiar las definiciones de ranuras zicsr y riscv32. Sin embargo, Este instructivo se enfoca en agregar las definiciones necesarias a riscv32i. ranura. Analicemos con más detalle lo que se define actualmente en este espacio:

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

En primer lugar, hay una sección includes {} que enumera los archivos de encabezado en el código generado cuando se haga referencia a este espacio de forma indirecta, en el ISA final. Los archivos de inclusión también pueden aparecer en una lista a la sección includes {} con alcance, en cuyo caso, siempre se incluyen. Esto puede sería útil si se tuviera que agregar el mismo archivo de inclusión a cada ranura definición.

Las declaraciones default size y default latency definen que, a menos que se especifica de otro modo, el tamaño de una instrucción es 4 y que la latencia de una la escritura del operando de destino es de 0 ciclos. Ten en cuenta que el tamaño de la instrucción que se especifica aquí, es el tamaño del incremento del contador del programa para calcular la de la siguiente instrucción secuencial que se ejecutará en la y un encargado del tratamiento de datos. Este valor puede o no ser el mismo que el tamaño en bytes del de instrucciones en el archivo ejecutable de entrada.

La sección del código de operación es fundamental para la definición del espacio. Como puedes ver, solo dos Los códigos de operación (instrucciones) fence y ebreak se definieron hasta ahora en riscv32i El código de operación fence se define especificando el nombre (fence) y la especificación del operando ({: imm12 : }), seguida del desensamblado opcional ("fence") y la función que admite llamadas que se vinculará como la semántica la función ("&RV32IFence").

Los operandos de instrucción se especifican como un triple, con cada componente separados por punto y coma, predicado “:” lista de operandos de origen ':' lista de operandos de destino. Las listas de operandos de origen y destino están en coma listas separadas de nombres de operandos. Como puedes ver, los operandos de instrucción para la instrucción fence contiene operandos de predicado, solo una fuente nombre de operando imm12 y sin operandos de destino. El subconjunto de RiscV RV32I no no admite la ejecución predicada, por lo que el operando del predicado siempre estará vacío en este instructivo.

La función semántica se especifica como la cadena necesaria para especificar el código C++ función o que admite llamadas para llamar a la función semántica. La firma del que admite llamadas es void(Instruction *).

La especificación de desensamblado consiste en una lista de cadenas separadas por comas. Normalmente, solo se usan dos cadenas, una para el código de operación y otra para el operandos. Cuando se les da formato (con la llamada AsString() en Instruction), cada string se formatea dentro de un campo según el disasm widths de la especificación descrita anteriormente.

Los siguientes ejercicios te ayudarán a agregar instrucciones al archivo riscv32i.isa suficiente para simular un mensaje de “Hello World” . Para quienes tienen prisa, puedes encontrar soluciones en riscv32i.isa y rv32i_instructions.h.


Realiza la compilación inicial

Si no cambiaste el directorio a riscv_isa_decoder, hazlo ahora. Después compilar el proyecto de la siguiente manera: esta compilación debería tener éxito.

$ cd riscv_isa_decoder
$ bazel build :all

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

En este directorio, entre otros archivos, encontrarás los siguientes elementos: archivos C++ generados:

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

Para analizar riscv32i_enums.h, haz clic en él en el navegador. Deberías verás que contiene algo como:

#ifndef RISCV32I_ENUMS_H
#define RISCV32I_ENUMS_H

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

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

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

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

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

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

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

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

  enum class AttributeEnum {
    kPastMaxValue = 0
  };

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

#endif  // RISCV32I_ENUMS_H

Como puedes ver, cada ranura, código de operación y operando que se definió en la El archivo riscv32i.isa se define en uno de los tipos de enumeración. Además, hay un array OpcodeNames que almacena todos los nombres de los códigos de operación (es definido en riscv32i_enums.cc). Los otros archivos contienen el decodificador generado, que se tratará con más detalle en otro instructivo.

Regla de compilación de Bazel

El destino del decodificador de ISA en Bazel se define mediante una macro de regla personalizada llamada mpact_isa_decoder, que se cargó desde mpact/sim/decoder/mpact_sim_isa.bzl en el repositorio mpact-sim. Para este instructivo, el objetivo de compilación definido en riscv_isa_decoder/BUILD es:

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

Esta regla llama al generador y al analizador de ISA para generar el código de C++. luego compila los datos generados en una biblioteca de la que pueden depender otras reglas la etiqueta //riscv_isa_decoder:riscv32i_isa Se usa la sección includes para especificar archivos .isa adicionales que el archivo fuente puede incluir. El isa_name se usa para especificar qué isa específica, obligatorio si hay más de uno especificado en el archivo de origen para el que se generará el decodificador.


Agrega instrucciones de registro y registro de ALU

Ahora es momento de agregar algunas instrucciones nuevas al archivo riscv32i.isa. La primera grupo de instrucciones son instrucciones de registro-registro de ALU, como add, and, etc. En RiscV32, todas usan el formato de instrucción binaria de tipo R:

31/25 24/20 19/15 14-12 11 a 7 6...
7 5 5 3 5 7
func7 rs2 rs1 func3 .o código de operación

Si bien el archivo .isa se usa para generar un decodificador independiente del formato, útil para tener en cuenta el formato binario y su diseño para guiar las entradas. Como hay tres campos que son relevantes para el decodificador que propaga la objetos de instrucción: rs2, rs1 y rd. En este punto, elegiremos usar estos nombres para registros de números enteros que están codificados del mismo modo (secuencias de bits), en los mismos campos y toda la instrucción.

Las instrucciones que vamos a agregar son las siguientes:

  • add: Suma de número entero.
  • and: a nivel de bits y.
  • or: O a nivel de bits.
  • sll: Desplazamiento lógico hacia la izquierda.
  • sltu: establecido menor que, sin firma.
  • sub: resta de números enteros.
  • xor: Xor a nivel de bits.

Cada una de estas instrucciones se agregará a la sección opcodes del Definición de ranura riscv32i. Recuerda que debemos especificar el nombre, los códigos de operación y la función semántica para cada instrucción. El nombre es fácil, usemos los nombres de los códigos de operación de arriba. Además, todas usan los mismos operandos, así que podemos usar { : rs1, rs2 : rd} para la especificación del operando. Esto significa que el operando de origen del registro especificado por rs1 tendrá el índice 0 en la fuente vector de operando en el objeto de instrucciones, el operando de origen del registro especificado de rs2 tendrá el índice 1 y el operando de destino de registro especificado por rd será el único elemento en el vector de operando de destino (en el índice 0).

A continuación, está la especificación de la función semántica. Esto se hace usando la palabra clave semfunc y una cadena de C++ que especifica una función que admite llamadas y que se puede usar para asignar a un std::function. En este instructivo, usaremos funciones, por lo que el elemento cadena será "&MyFunctionName". Si usas el esquema de nombres sugerido por el fence, deben ser "&RV32IAdd", "&RV32IAnd", etcétera.

Por último, está la especificación de desensamblado. Comienza con la palabra clave disasm y seguido de una lista de cadenas separadas por comas que especifica las instrucciones se deben imprimir como una cadena. Con un letrero % frente a una el nombre del operando indica una sustitución de cadena usando la representación de cadena de ese operando. Para la instrucción add, esto sería: disasm: "add", "%rd, %rs1,%rs2". Esto significa que la entrada para la instrucción add debe verse como:

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

Edita el archivo riscv32i.isa y agrega todas estas instrucciones al archivo .isa descripción. Si necesitas ayuda (o quieres revisar tu trabajo), puedes el archivo de descripción es aquí.

Una vez que se agreguen las instrucciones al archivo riscv32i.isa, será necesario agregar declaraciones de funciones para cada una de las nuevas funciones semánticas que se se hace referencia al archivo rv32i_instructions.h ubicado en `../semantic_functions/. De nuevo, si necesitas ayuda (o quieres revisar tu trabajo), la respuesta es aquí.

Cuando termines, regresa a riscv_isa_decoder y vuelve a compilar. Si lo deseas, puedes examinar los archivos fuente generados.


Agregar instrucciones de ALU con instrucciones inmediatas

El siguiente conjunto de instrucciones que agregaremos son instrucciones de ALU que usan un un valor inmediato en lugar de uno de los registros. Existen tres grupos de estos instrucciones (basadas en el campo inmediato): las instrucciones inmediatas de I-Type con una firma de 12 bits inmediata, las instrucciones especializadas inmediatas de tipo I para los cambios, y el tipo U inmediato, con un valor inmediato de 20 bits sin firma. Los formatos se muestran a continuación:

El formato inmediato I-Type:

31/20 19/15 14-12 11 a 7 6...
12 5 3 5 7
imm12 rs1 func3 .o código de operación

El formato inmediato especializado I-Type:

31/25 24/20 19/15 14-12 11 a 7 6...
7 5 5 3 5 7
func7 uimm5 rs1 func3 .o código de operación

El formato inmediato tipo U:

31-12 11 a 7 6...
20 5 7
uimm20 .o código de operación

Como puedes ver, los nombres de operandos rs1 y rd hacen referencia a los mismos campos de bits que previamente y se usan para representar registros de números enteros, por lo que estos nombres pueden en los que se retienen. Los campos de valor inmediato tienen diferentes longitudes y ubicaciones. dos (uimm5 y uimm20) no están firmados, mientras que imm12 está firmado. Cada uno de estos usarán su propio nombre.

Por lo tanto, los operandos para las instrucciones de tipo I deben ser { : rs1, imm12 :rd }. Para las instrucciones especializadas de I-Type, debe ser { : rs1, uimm5 : rd}. La especificación del operando de instrucción del tipo U debe ser { : uimm20 : rd }.

Las instrucciones de I-Type que debemos agregar son:

  • addi: Se agrega inmediatamente.
  • andi: bit a bit y con inmediato.
  • ori: bit a bit o con inmediato.
  • xori: Es un xor bit a bit con inmediato.

Las instrucciones especializadas de I-Type que debemos agregar son:

  • slli: Desplaza a la izquierda un lógico de inmediato.
  • srai: Desplaza la aritmética hacia la derecha de manera inmediata.
  • srli: Desplaza hacia la derecha de manera lógica.

Las instrucciones sobre el tipo de U que debemos agregar son las siguientes:

  • auipc: Agrega el valor inmediato superior al CPC.
  • lui: Carga la parte superior inmediatamente.

Los nombres que se deben usar para los códigos de operación se derivan de los nombres de las instrucciones de forma natural. arriba (no es necesario inventar algunos nuevos, ya que todos son únicos). Cuando se trata de especificando las funciones semánticas, recuerda que los objetos de instrucción codifican interfaces para los operandos de origen que son independientes del operando subyacente el tipo de letra. Esto significa que, en el caso de las instrucciones que tienen la misma operación, puede diferir en los tipos de operandos, puede compartir la misma función semántica. Por ejemplo: la instrucción addi realiza la misma operación que la instrucción add si uno ignora el tipo de operando, por lo que pueden usar la misma función semántica la especificación "&RV32IAdd". Del mismo modo que para andi, ori, xori y slli. Las otras instrucciones usan nuevas funciones semánticas, pero deben tener el nombre basada en la operación, no en operandos, por lo tanto, para srai, usa "&RV32ISra". El Las instrucciones de tipo U auipc y lui no tienen equivalentes de registro, así que está bien para usar "&RV32IAuipc" y "&RV32ILui".

Las cadenas de desarmado son muy similares a las del ejercicio anterior, como es de esperar, las referencias a %rs2 se reemplazan por %imm12, %uimm5 o %uimm20, según corresponda.

Realiza los cambios y compila. Verifica el resultado generado. Tal como previamente, puedes comparar tu trabajo con riscv32i.isa y las rv32i_instructions.h.


Las instrucciones de rama y de salto y enlace que necesitamos para agregar usan un destino. operando que solo está implícito en la propia instrucción, es decir, la siguiente pc valor. En esta etapa, trataremos esto como un operando adecuado con el nombre next_pc Se definirá con mayor detalle en un instructivo posterior.

Instrucciones para la sucursal

Todas las ramas que estamos agregando usan la codificación tipo B.

31 30-25 24/20 19/15 14-12 11 a 8 7 6...
1 6 5 5 3 4 1 7
MM MM rs2 rs1 func3 MM MM código de operación

Los diferentes campos inmediatos se concatenan en un mensaje inmediato de 12 bits con firma valor. Dado que el formato no es realmente relevante, lo llamaremos inmediatamente bimm12, para la rama de 12 bits inmediata. La fragmentación se abordará el siguiente instructivo sobre la creación del decodificador binario. Todas las las instrucciones de la rama comparan los registros de números enteros especificados por rs1 y rs2, si la condición es verdadera, el valor inmediato se suma al valor de PC actual para producir la dirección de la siguiente instrucción que se ejecutará. Los operandos para el por lo tanto, las instrucciones de la bifurcación deben ser { : rs1, rs2, bimm12 : next_pc }.

Las instrucciones de la rama que debemos agregar son las siguientes:

  • beq: Rama si es igual.
  • bge: Rama si es mayor o igual que.
  • bgeu: Rama si es mayor o igual sin signo.
  • blt: Rama si es menor que.
  • bltu: Rama si es menor que sin firmar.
  • bne: Rama si no es igual.

Todos estos nombres de código de operación son únicos, por lo que se pueden volver a usar en .isa. descripción. Por supuesto, se deben agregar nuevos nombres de funciones semánticas, p.ej., "&RV32IBeq", etcétera

La especificación de desensamblado ahora es un poco más complicada, ya que la dirección de la la instrucción se usa para calcular el destino, sin que en realidad sea parte de los operandos de instrucción. Sin embargo, es parte de la información almacenada en el objeto de instrucción, por lo que está disponible. La solución es usar la sintaxis de expresión en la cadena de desensamblado. En lugar de usar "%" seguido de el nombre del operando, puedes escribir %(expression: print format). Solo muy sencillo se admiten expresiones, pero la dirección más el desplazamiento es una de ellas, con el @ símbolo utilizado para la dirección actual de la instrucción. El formato de impresión es similar a Formatos de printf de estilo C, pero sin el % inicial. El formato de desensamblado para la instrucción beq se convierte en lo siguiente:

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

Solo se deben agregar dos instrucciones de salto y vinculación: jal (salto y vinculación) y jalr (Salto y vínculo indirectos).

La instrucción jal usa la codificación tipo J:

31 30-21 20 19/12 11 a 7 6...
1 10 1 8 5 7
MM MM MM MM .o código de operación

Al igual que con las instrucciones de la rama, el inmediato de 20 bits se fragmenta varios campos, por lo que lo llamaremos jimm20. La fragmentación no es importante pero abordaremos esto en el próximo instructivo sobre la creación del decodificador binario. El operando especificación se convierte en { : jimm20 : next_pc, rd }. Ten en cuenta que hay dos operandos de destino, el siguiente valor de pc y el registro de enlace especificado en la instrucciones.

Al igual que las ramas de instrucciones anteriores, el formato de desensamblado se convierte en lo siguiente:

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

El salto y vínculo indirecto usa el formato I-Type con el inmediato de 12 bits. Integra agrega el valor inmediato de signo extendido al registro de números enteros especificado por rs1 para producir la dirección de las instrucciones de destino. El registro de vínculos es registro de números enteros especificado por rd.

31/20 19/15 14-12 11 a 7 6...
12 5 3 5 7
imm12 rs1 func3 .o código de operación

Si has visto el patrón, ahora deducirías que la especificación del operando para jalr debe ser { : rs1, imm12 : next_pc, rd }, y el desensamblado especificación:

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

Realiza los cambios y, luego, compila. Verifica el resultado generado. Justo como antes, puedes comparar tu trabajo con riscv32i.isa y rv32i_instructions.h.


Agregar instrucciones de la tienda

Las instrucciones de la tienda son muy sencillas. Todas usan el formato tipo S:

31/25 24/20 19/15 14-12 11 a 7 6...
7 5 5 3 5 7
MM rs2 rs1 func3 MM código de operación

Como puedes ver, este es otro caso de un inmediato fragmentado de 12 bits. llámalo simm12. Todas las instrucciones de almacenamiento almacenan el valor del número entero registro especificado por rs2 a la dirección efectiva en la memoria que se obtiene sumando el valor del registro de números enteros especificado por rs1 al valor de signo extendido de el de 12 bits. El formato de operando debe ser { : rs1, simm12, rs2 } para todas las instrucciones de la tienda.

Las instrucciones de almacenamiento que deben implementarse son las siguientes:

  • sb: Almacena bytes.
  • sh: Almacena media palabra.
  • sw: Palabra de la tienda.

La especificación de desensamblado para sb es la que debes esperar:

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

Las especificaciones de la función semántica también son las que esperarías: "&RV32ISb", etcétera

Realiza los cambios y, luego, compila. Verifica el resultado generado. Justo como antes, puedes comparar tu trabajo con riscv32i.isa y rv32i_instructions.h.


Agregar instrucciones de carga

Las instrucciones de carga se modelan de manera un poco diferente a otras instrucciones en el simulador. Para poder modelar casos en los que la latencia de carga es inciertos, las instrucciones de carga se dividen en dos acciones separadas: 1) el procesamiento de direcciones y el acceso a la memoria, y 2) la reescritura de resultados. En la simulador, esto se hace dividiendo la acción semántica de la carga en dos instrucciones separadas, la instrucción principal y una instrucción secundaria. Además, cuando especificamos operandos, debemos especificarlos para el principal y el child. Esto se hace tratando la especificación del operando como una lista de tresillos. La sintaxis es:

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

Todas las instrucciones de carga usan el formato I-Type al igual que muchas de las instrucciones instrucciones:

31/20 19/15 14-12 11 a 7 6...
12 5 3 5 7
imm12 rs1 func3 .o código de operación

La especificación del operando divide los operandos necesarios para calcular la dirección y, luego, iniciar el acceso a la memoria desde el destino de registro para los datos de carga: {( : rs1, imm12 : ), ( : : rd) }

Como la acción semántica se divide en dos instrucciones, las funciones semánticas de manera similar debes especificar dos funciones que admiten llamadas. Para lw (cargar palabra), esto sería escrito:

    semfunc: "&RV32ILw", "&RV32ILwChild"

La especificación de desensamblado es más convencional. No se hace mención de educación infantil. En lw, debería ser la siguiente:

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

Las instrucciones de carga que se deben implementar son las siguientes:

  • lb: byte de carga.
  • lbu: carga de bytes sin firmar.
  • lh: carga media palabra.
  • lhu: Carga media palabra sin firma.
  • lw: carga de palabra.

Realiza los cambios y, luego, compila. Verifica el resultado generado. Justo como antes, puedes comparar tu trabajo con riscv32i.isa y rv32i_instructions.h.

Gracias por haber llegado hasta aquí. Esperamos que esta información te resulte útil.