Decodificatore ISA RiscV

Gli obiettivi di questo tutorial sono:

  • Scopri come vengono rappresentate le istruzioni nel simulatore MPACT-Sim.
  • Scopri la struttura e la sintassi del file di descrizione ISA.
  • Scrivi le descrizioni ISA per il sottoinsieme RiscV RV32I di istruzioni

Panoramica

In MPACT-Sim le istruzioni della destinazione vengono decodificate e memorizzate in una per rendere più disponibili le informazioni e la semantica l'esecuzione è più veloce. Le istanze di queste istruzioni vengono memorizzate nella cache in un in modo da ridurre il numero di volte in cui le istruzioni eseguite di frequente eseguito.

Il corso di istruzione

Prima di iniziare, è utile dare un'occhiata a come vengono le istruzioni rappresentati in MPACT-Sim. La classe Instruction è definita in mpact-sim/mpact/sim/generic/instruction.h.

L'istanza della classe Instruction contiene tutte le informazioni necessarie simulare l'istruzione quando viene "eseguita", ad esempio:

  1. Indirizzo dell'istruzione, dimensioni simulate dell'istruzione, ovvero dimensioni in .text.
  2. Codice operativo istruzione.
  3. Puntatore dell'interfaccia operando del predicato (se applicabile).
  4. Vettore dei puntatori dell'interfaccia dell'operando di origine.
  5. Vettore dei puntatori dell'interfaccia dell'operando di destinazione.
  6. Funzione semantica richiamabile.
  7. Puntatore all'oggetto di stato dell'architettura.
  8. Puntatore all'oggetto di contesto.
  9. Puntatore alle istanze di istruzione figlio e successive.
  10. Stringa disassemblaggio.

Queste istanze sono generalmente archiviate in una cache di istruzioni (istanza) e riutilizzate ogni volta che l'istruzione viene eseguita nuovamente. Questo migliora il rendimento durante il runtime.

Ad eccezione del puntatore all'oggetto di contesto, tutti vengono compilati dalla classe decodificatore di istruzioni generato dalla descrizione ISA. Per questo tutorial non è necessario conoscere i dettagli di questi articoli in quanto non di usarle direttamente. Per avere una conoscenza approfondita di come vengono utilizzati, sufficienti.

La funzione semantica richiamabile è l'oggetto funzione/metodo/funzione C++ (incluse lambdas) che implementa la semantica dell'istruzione. Per per un'istruzione add, carica ogni operando di origine e aggiunge i due e scrive il risultato in un singolo operando di destinazione. L'argomento funzioni semantiche è trattata in dettaglio nel tutorial sulle funzioni semantiche.

Operatori di istruzione

La classe di istruzioni include puntatori a tre tipi di interfacce operando: predicato, origine e destinazione. Queste interfacce consentono alle funzioni semantiche essere scritte indipendentemente dal tipo effettivo dell'istruzione sottostante operando. Ad esempio, l'accesso ai valori di registri e immediati attraverso la stessa interfaccia. Ciò significa che le istruzioni che eseguono ma su diversi operandi (ad es. registri e immediati) possono essere implementato utilizzando la stessa funzione semantica.

L'interfaccia operando dei predicati, per gli ISA che supportano esecuzione di un'istruzione di controllo (per altri ISA è nullo), viene utilizzata per determinare se una data istruzione deve essere eseguita in base al valore booleano del predicato.

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

L'interfaccia dell'operando di origine consente alla funzione semantica dell'istruzione di leggere dagli operandi delle istruzioni indipendentemente dall'operando sottostante di testo. I metodi dell'interfaccia supportano operandi sia scalari che vettoriali.

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

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

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

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

  virtual ~SourceOperandInterface() = default;
};

L'interfaccia dell'operando di destinazione fornisce i metodi per l'allocazione e la gestione DataBuffer istanze (il tipo di dati interno utilizzato per archiviare i valori di registro). R l'operando di destinazione ha anche una latenza associata, ovvero il numero di cicli di attesa fino a quando l'istanza di buffer dei dati allocata dall'istruzione funzione semantica viene utilizzata per aggiornare il valore del registro di destinazione. Per un'istruzione add, la latenza può essere 1, mentre per un'istruzione mpy potrebbe essere 4. Questo argomento viene trattato più dettagliatamente nei tutorial sulle funzioni semantiche.

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

Descrizione ISA

L'ISA (Instruction Set Architecture) di un processore definisce il modello astratto mediante il quale il software interagisce con l'hardware. Definisce l'insieme le istruzioni disponibili, i tipi di dati, i registri e altro stato istruzioni e il loro comportamento (semantica). Ai fini di MPACT-Sim, l'ISA non include l'effettiva codifica delle istruzioni. che viene trattata separatamente.

L'ISA del processore è espresso in un file di descrizione che descrive di istruzioni impostate a un livello astratto e indipendente dalla codifica. Il file di descrizione indica l'insieme di istruzioni disponibili. Per ogni istruzione è obbligatorio elencare il nome, il numero e i nomi dei suoi operandi e i relativi associazione a una funzione/chiamabile C++ che ne implementa la semantica. Inoltre, è possibile specificare una stringa di formattazione per lo smontaggio e l'uso da parte dell'istruzione dei nomi delle risorse hardware. Il primo è utile per produrre un testo rappresentazione dell'istruzione per il debug, il tracciamento o l'uso interattivo. La quest'ultima può essere utilizzata per migliorare la precisione dei cicli nella simulazione.

Il file di descrizione ISA viene analizzato dallo strumento isa-parser, che genera il codice per il decoder dell'istruzione indipendente dalla rappresentazione. Questo decoder è responsabile compilare i campi degli oggetti dell'istruzione. I valori specifici, ad esempio numero di registro di destinazione, sono ottenuti da un'istruzione di formato specifica decodificatore. Uno di questi decoder è il decoder binario, che è l'elemento principale nel prossimo tutorial.

Questo tutorial spiega come scrivere un file di descrizione ISA per un modello dell'architettura. Useremo un sottoinsieme dell'istruzione RiscV RV32I impostata illustrerò questo aspetto e, insieme agli altri tutorial, costruisci un simulatore in grado di simulare un "Hello World" . Per maggiori dettagli sull'ISA di RiscV, consulta Specifiche Risc-V.

Per iniziare, apri il file: riscv_isa_decoder/riscv32i.isa

I contenuti del file sono suddivisi in più sezioni. Il primo è l'ISA dichiarazione:

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

In questo modo viene dichiarato che RiscV32I è il nome dell'ISA e il generatore di codice crea una classe denominata RiscV32IEncodingBase che definisce l'interfaccia il decoder generato utilizzerà per ottenere informazioni su opcode e operando. Il nome del questa classe viene generata convertendo il nome ISA in Pascal-case, quindi concatenandola con EncodingBase. La dichiarazione slots { riscv32; } specifica che c'è un solo slot di istruzioni riscv32 in RiscV32I ISA (al contrario di slot multipli in un'istruzione VLIW) e che l'unico istruzioni valide sono quelle definite per l'esecuzione in riscv32.

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

Ciò specifica che il primo frammento di disassemblaggio (vedi di seguito) saranno giustificati in un formato di 15 caratteri in campo ampio. Eventuali frammenti successivi verranno aggiunti a questo campo senza eventuali spazi aggiuntivi.

Sotto ci sono tre dichiarazioni di slot: riscv32i, zicsr e riscv32. In base alla definizione di isa sopra riportata, solo le istruzioni definite per riscv32 farà parte dell'isa RiscV32I. A cosa servono gli altri due spazi?

Gli slot possono essere utilizzati per suddividere le istruzioni in gruppi separati, che a loro volta possono essere combinati in un'unica area alla fine. Nota la notazione : riscv32i, zicsr nella dichiarazione slot riscv32. Specifica che l'area riscv32 eredita tutte le istruzioni definite negli slot zicsr e riscv32i. Il servizio ISA a 32 bit di RiscV è costituito da un ISA di base denominato RV32I, a cui è possibile aggiungere una serie di estensioni facoltative da aggiungere. Il meccanismo di slot consente di visualizzare le istruzioni in queste estensioni specificati separatamente e poi combinati in base alle esigenze per definire complessivo dell'ISA. In questo caso, le istruzioni nella colonna RiscV "I" gruppo definito separatamente da quelli dello "zicsr" gruppo. Potrebbero essere definiti gruppi aggiuntivi per "M" (moltiplica/divide), "F" (rappresentazione in virgola mobile a precisione singola), "D" (rappresentazione in virgola mobile a precisione doppia), "C" (istruzioni compatte a 16 bit) e così via, necessari per l'ISA RiscV finale desiderato.

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

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

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

Le definizioni degli slot zicsr e riscv32 non devono essere modificate. Tuttavia, l'attenzione di questo tutorial è l'aggiunta delle definizioni necessarie al riscv32i slot machine. Diamo un'occhiata più da vicino a ciò che è attualmente definito in questo spazio:

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

Innanzitutto, c'è una sezione includes {} che elenca i file di intestazione da includere nel codice generato quando viene fatto riferimento a questo spazio, direttamente o indirettamente, nell'ISA finale. I file inclusi possono essere elencati anche in una sezione includes {} con ambito, nel qual caso vengono sempre incluse. Questo può utile se lo stesso file di inclusione deve essere aggiunto a ogni area definizione di Kubernetes.

Le dichiarazioni default size e default latency definiscono che, a meno che altrimenti specificato, la dimensione di un'istruzione è 4 e la latenza la scrittura dell'operando di destinazione è pari a 0 cicli. Nota: le dimensioni dell'istruzione qui specificata, corrisponde alla dimensione dell'incremento del contatore di programma per calcolare della successiva istruzione sequenziale da eseguire di fatturazione. Può essere o meno la stessa delle dimensioni in byte del rappresentazione dell'istruzione nel file eseguibile di input.

Al centro della definizione dell'area c'è la sezione opcode. Come puoi vedere, solo due finora sono stati definiti i codici operativi (istruzioni) fence e ebreak in riscv32i. L'opcode fence viene definito specificando il nome (fence) e la specifica dell'operando ({: imm12 : }), seguito dallo smontaggio facoltativo ("fence") e il richiamo da associare come semantico ("&RV32IFence").

Gli operandi dell'istruzione sono specificati come tripli, con ogni componente separate da un punto e virgola, predicato ':' elenco operandi di origine ':' elenco operando di destinazione. Gli elenchi di operandi di origine e di destinazione sono separati da virgole elenchi separati di nomi operandi. Come puoi vedere, gli operandi dell'istruzione l'istruzione fence contiene, senza operandi dei predicati, solo una singola origine nome dell'operando imm12 e nessun operando di destinazione. Il sottoinsieme RiscV RV32I non supporta l'esecuzione predicata, pertanto l'operando del predicato sarà sempre vuoto in questo tutorial.

La funzione semantica è specificata come la stringa necessaria per specificare il C++ o richiamabile per richiamare la funzione semantica. La firma del funzione semantica/chiamabile è void(Instruction *).

La specifica di disassemblaggio consiste in un elenco di stringhe separate da virgole. Solitamente vengono utilizzate solo due stringhe, una per l'opcode e una per il valore operandi. Se formattate (utilizzando la chiamata AsString() in Istruzione), ogni la stringa viene formattata all'interno di un campo secondo disasm widths la specifica descritta sopra.

I seguenti esercizi ti aiutano ad aggiungere istruzioni al file riscv32i.isa sufficienti per simulare un "Hello World" . Per chi va di fretta, sono disponibili in riscv32i.isa e rv32i_instructions.h.


Esegui build iniziale

Se non hai impostato la directory riscv_isa_decoder, fallo ora. Poi creare il progetto nel seguente modo. Questa build dovrebbe riuscire.

$ cd riscv_isa_decoder
$ bazel build :all

Ora reimposta la directory alla radice del repository, quindi diamo un'occhiata alle origini generate. Per farlo, cambia la directory in bazel-out/k8-fastbuild/bin/riscv_isa_decoder (supponendo che tu stia utilizzando un dispositivo x86) host - per altri host, k8-fastbuild sarà un'altra stringa).

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

In questa directory, tra gli altri file, sono presenti i seguenti file C++ generati:

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

Diamo un'occhiata a riscv32i_enums.h facendo clic nel browser. Dovresti vediamo che contiene qualcosa del tipo:

#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

Come puoi vedere, ogni area, opcode e operando definiti Il file riscv32i.isa è definito in uno dei tipi di enumerazione. Inoltre, c'è un array OpcodeNames che memorizza tutti i nomi dei codici operativi (è definita in riscv32i_enums.cc). Gli altri file contengono il decoder generato, che verrà illustrato più nel dettaglio in un altro tutorial.

Regola di build Bazel

Il target del decoder ISA in Bazel viene definito utilizzando una macro delle regole personalizzate denominata mpact_isa_decoder, che viene caricato da mpact/sim/decoder/mpact_sim_isa.bzl nel repository mpact-sim. Per questo tutorial, il target di build definito in riscv_isa_decoder/BUILD è:

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

Questa regola chiama lo strumento e il generatore dell'analizzatore sintattico ISA per generare il codice C++, quindi compila i dati generati in una libreria da cui possono dipendere altre regole, l'etichetta //riscv_isa_decoder:riscv32i_isa. Viene utilizzata la sezione includes per specificare altri file .isa che possono essere inclusi nel file di origine. La isa_name viene utilizzato per specificare quale ISA specifica, obbligatoria se più di una specificato, nel file sorgente per il quale generare il decoder.


Aggiungi istruzioni ALU per la registrazione e la registrazione

Ora è il momento di aggiungere alcune nuove istruzioni al file riscv32i.isa. Il primo gruppo di istruzioni sono istruzioni ALU registrate e registrate, come add, and e così via. Su RiscV32, tutti usano il formato dell'istruzione binaria di tipo R:

31,25 24/20 19/15 14..12 11..7 6,0
7 5 5 3 5 7
func7 rs2 rs1 func3 ° opcode, codice operativo

Anche se il file .isa viene utilizzato per generare un decoder indipendente dal formato, è comunque è utile considerare il formato binario e il suo layout per guidare le voci. Man mano che puoi vedere, ci sono tre campi rilevanti per il decoder che popolano oggetti dell'istruzione: rs2, rs1 e rd. A questo punto sceglieremo di usare questi nomi per registri interi che sono codificati allo stesso modo (sequenze di bit), negli stessi campi di istruzioni, in tutte le istruzioni.

Le istruzioni che aggiungeremo sono le seguenti:

  • add - aggiunta di numeri interi.
  • and: bit a bit e.
  • or: bit a bit o.
  • sll: sposta verso sinistra in modo logico.
  • sltu: imposta meno di, non firmato.
  • sub: numero intero sottratto.
  • xor: xor a livello di bit.

Ognuna di queste istruzioni verrà aggiunta alla sezione opcodes del Definizione dell'area riscv32i. Ricorda che dobbiamo specificare il nome, i codici operativi, e la funzione semantica per ogni istruzione. Il nome è facile, usiamo solo i nomi opcode riportati sopra. Inoltre, usano tutti gli stessi operandi, possiamo usare { : rs1, rs2 : rd} per la specifica dell'operando. Ciò significa che l'operando della sorgente del registro specificato da rs1 avrà l'indice 0 nella vettore di operando nell'oggetto di istruzione, l'operando di origine del registro specificato da rs2 avrà l'indice 1 e l'operando di destinazione del registro specificato da rd sarà l'unico elemento nel vettore operando di destinazione (con indice 0).

Poi c'è la specifica della funzione semantica. Questo viene fatto utilizzando la parola chiave semfunc e una stringa C++ che specifica un elemento richiamabile che può essere utilizzato per assegnare a std::function. In questo tutorial utilizzeremo le funzioni, quindi la funzione sarà "&MyFunctionName". Utilizzando lo schema di denominazione suggerito dal fence, devono essere "&RV32IAdd", "&RV32IAnd" e così via.

L'ultima è la specifica di disassemblaggio. Inizia con la parola chiave disasm e è seguito da un elenco di stringhe separate da virgole che specifica come l'istruzione deve essere stampata come stringa. Uso del simbolo % davanti a un indica una sostituzione di stringa utilizzando la rappresentazione in formato stringa di per quell'operando. Per l'istruzione add, il valore corrisponde a: disasm: "add", "%rd, %rs1,%rs2". Ciò significa che la voce per l'istruzione add deve avere come:

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

Modifica il file riscv32i.isa e aggiungi tutte queste istruzioni alla Descrizione .isa. Se hai bisogno di aiuto (o vuoi controllare il tuo lavoro), il file di descrizione è qui

Una volta aggiunte le istruzioni al file riscv32i.isa, sarà necessario di aggiungere dichiarazioni di funzione per ciascuna delle nuove funzioni semantiche fa riferimento al file rv32i_instructions.h che si trova in '../semantic_functions/. Anche in questo caso, se hai bisogno di aiuto (o vuoi controllare il tuo lavoro), la risposta è qui

Terminata questa operazione, passa di nuovo all'riscv_isa_decoder nella directory corrente e ricrearlo. Puoi esaminare i file di origine generati.


Aggiungi istruzioni ALU con immediate

La prossima serie di istruzioni che aggiungeremo è costituita da istruzioni ALU che utilizzano un valore immediato invece che uno dei registri. Esistono tre gruppi di questi istruzioni (in base al campo immediato): le istruzioni immediate tipo I-Type con firma immediata a 12 bit, le istruzioni specializzate in I-Type per gli spostamenti e U-Type immediato, con un valore immediato senza segno a 20 bit. Di seguito sono riportati i formati:

Formato immediato I-Type:

31/20 19/15 14..12 11..7 6,0
12 5 3 5 7
imm12 rs1 func3 ° opcode, codice operativo

Il formato immediato I-Type specializzato:

31,25 24/20 19/15 14..12 11..7 6,0
7 5 5 3 5 7
func7 uimm5 rs1 func3 ° opcode, codice operativo

Il formato immediato di U-Type:

31.12 11..7 6,0
20 5 7
uimm20 ° opcode, codice operativo

Come puoi vedere, i nomi degli operandi rs1 e rd si riferiscono agli stessi campi di bit in precedenza e sono utilizzati per rappresentare registri interi, quindi questi nomi possono essere vengono mantenuti. I campi dei valori immediati sono di lunghezza e posizione diverse e due (uimm5 e uimm20) non sono firmati, mentre imm12 è firmato. Ciascuno di che utilizzeranno il proprio nome.

Gli operandi per le istruzioni I-Type devono quindi essere { : rs1, imm12 :rd }. Per le istruzioni I-Type specializzate, il valore deve essere { : rs1, uimm5 : rd}. La specifica dell'operando dell'istruzione U-Type deve essere { : uimm20 : rd }.

Le istruzioni I-Type che dobbiamo aggiungere sono:

  • addi - Aggiungi subito.
  • andi: a livello di bit e con immediatezza.
  • ori: bit a bit o con immediatezza.
  • xori: xor a livello di bit con effetto immediato.

Le istruzioni I-Type specializzate che dobbiamo aggiungere sono:

  • slli - Sposta la logica verso sinistra immediatamente.
  • srai: sposta l'aritmetica a destra per immediata.
  • srli: sposta subito verso destra la logica.

Le istruzioni per il tipo U che dobbiamo aggiungere sono:

  • auipc - Aggiungi immediatamente superiore al pc.
  • lui - Carica immediatamente la parte superiore della pagina.

I nomi da usare per i codici operativi seguono naturalmente i nomi delle istruzioni. sopra (non c'è bisogno di trovarne di nuovi, sono tutti unici). Per quanto riguarda specificando le funzioni semantiche, ricorda che gli oggetti di istruzione codificano interfacce agli operandi di origine indipendenti dall'operando sottostante di testo. Ciò significa che per le istruzioni che hanno la stessa operazione, ma può variare nei tipi di operando, può condividere la stessa funzione semantica. Ad esempio, l'istruzione addi esegue la stessa operazione dell'istruzione add se Se viene ignorato il tipo di operando, è possibile utilizzare la stessa funzione semantica la specifica "&RV32IAdd". Allo stesso modo per andi, ori, xori e slli. Le altre istruzioni usano nuove funzioni semantiche, ma devono essere denominate in base all'operazione, non agli operandi, quindi per srai usa "&RV32ISra". La Le istruzioni di tipo U auipc e lui non hanno equivalenti nel registro, quindi è possibile per utilizzare "&RV32IAuipc" e "&RV32ILui".

Le stringhe per lo smontaggio sono molto simili a quelle dell'esercizio precedente, ma come previsto, i riferimenti a %rs2 vengono sostituiti con %imm12, %uimm5, o %uimm20, a seconda dei casi.

Apporta le modifiche e crea. Controlla l'output generato. Esattamente come In precedenza, puoi verificare il tuo lavoro riscv32i.isa e ai rv32i_instructions.h.


Le istruzioni di ramo e jump-and-link che devono essere aggiunte per aggiungere entrambe utilizzano una destinazione operando implicito soltanto nell'istruzione stessa, ossia nel pc successivo valore. In questa fase, lo tratteremo come un operando appropriato, con il nome next_pc. Verrà definita ulteriormente in un tutorial successivo.

Istruzioni ramo

Tutti i rami a cui aggiungiamo hanno la codifica B-Type.

31 30:00 24/20 19/15 14..12 11,8 7 6,0
1 6 5 5 3 4 1 7
imm imm rs2 rs1 func3 imm imm opcode, codice operativo

I diversi campi immediati sono concatenati in un messaggio immediato a 12 bit valore. Poiché il formato non è realmente pertinente, lo chiameremo immediatamente bimm12, per ramo a 12 bit immediato. La frammentazione verrà affrontata in nel prossimo tutorial sulla creazione del decoder binario. Tutti i le istruzioni di ramo confrontano i registri interi specificati da rs1 e rs2, se la condizione è true, il valore immediato viene aggiunto al valore pc corrente per produrre l'indirizzo dell'istruzione successiva da eseguire. Gli operandi per le istruzioni del ramo devono quindi essere { : rs1, rs2, bimm12 : next_pc }.

Le istruzioni per i rami che dobbiamo aggiungere sono:

  • beq: ramo se uguale.
  • bge - Ramo se maggiore o uguale a.
  • bgeu - Ramo se maggiore o uguale a senza firma.
  • blt - Ramo se inferiore a.
  • bltu - Ramo se è inferiore a quello non firmato.
  • bne: ramo se diverso.

Questi nomi opcode sono tutti univoci, quindi possono essere riutilizzati in .isa o l'audiodescrizione. Ovviamente, è necessario aggiungere nuovi nomi di funzioni semantiche, ad esempio "&RV32IBeq" e così via.

Le specifiche di disassemblaggio sono ora un po' più complicate, poiché l'indirizzo del viene utilizzata per calcolare la destinazione, senza che faccia parte degli operandi dell'istruzione. Tuttavia, fa parte delle informazioni memorizzate dell'oggetto istruzione, in modo che sia disponibile. La soluzione è utilizzare nella stringa di disassemblaggio. Invece di utilizzare "%" seguito da il nome dell'operando, puoi digitare %(expression: formato di stampa). Molto semplici sono supportate, ma indirizzo più offset sono una di queste, con il parametro @ simbolo utilizzato per l'indirizzo dell'istruzione corrente. Il formato di stampa è simile a Formati di stampa in stile C, ma senza il % iniziale. Il formato di disassemblaggio l'istruzione beq diventa quindi:

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

Devono essere aggiunte solo due istruzioni di collegamento rapido: jal (jump-and-link) e jalr (Salta e collega indiretto).

L'istruzione jal utilizza la codifica J-Type:

31 30/21 20 19/12 11..7 6,0
1 10 1 8 5 7
imm imm imm imm ° opcode, codice operativo

Proprio come per le istruzioni del ramo, l'immediato a 20 bit è frammentato in più campi, quindi gli assegniamo il nome jimm20. La frammentazione non è importante in questo momento, ma verrà affrontata nel prossimo tutorial sulla creazione del decoder binario. L'operando la specifica diventa { : jimm20 : next_pc, rd }. Tieni presente che ci sono due gli operandi di destinazione, il successivo valore pc e il registro di collegamento specificati istruzioni.

Analogamente alle istruzioni per i rami precedenti, il formato di smontaggio diventa:

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

Il jump-and-link indiretto utilizza il formato I-Type con immediatamente a 12 bit. it aggiunge il valore immediato esteso del segno al registro intero specificato da rs1 per produrre l'indirizzo dell'istruzione di destinazione. Il registro dei link è registro di numeri interi specificato da rd.

31/20 19/15 14..12 11..7 6,0
12 5 3 5 7
imm12 rs1 func3 ° opcode, codice operativo

Se hai visto il pattern ora deduci che la specifica dell'operando di jalr deve essere { : rs1, imm12 : next_pc, rd } e lo smontaggio specifiche:

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

Apporta le modifiche e crea. Controlla l'output generato. Soltanto Come in precedenza, puoi verificare il tuo lavoro riscv32i.isa e rv32i_instructions.h.


Aggiungi istruzioni per il negozio

Le istruzioni per i negozi sono molto semplici. Usano tutti il formato S-Type:

31,25 24/20 19/15 14..12 11..7 6,0
7 5 5 3 5 7
imm rs2 rs1 func3 imm opcode, codice operativo

Come abbiamo visto, si tratta di un altro caso di frammentazione immediata a 12 bit, chiamalo simm12. Le istruzioni per lo store memorizzano tutte il valore del numero intero specificato da rs2 all'indirizzo effettivo in memoria ottenuto aggiungendo il valore del registro intero specificato da rs1 al valore esteso del segno di immediatamente a 12 bit. Il formato dell'operando deve essere { : rs1, simm12, rs2 } per tutte le istruzioni del negozio.

Le istruzioni del datastore che devono essere implementate sono:

  • sb: byte del negozio.
  • sh: memorizza mezza parola.
  • sw - Parola del negozio.

La specifica di disassemblaggio di sb è quella prevista:

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

Anche le specifiche della funzione semantica sono quelle previste: "&RV32ISb", ecc.

Apporta le modifiche e crea. Controlla l'output generato. Soltanto Come in precedenza, puoi verificare il tuo lavoro riscv32i.isa e rv32i_instructions.h.


Aggiungi istruzioni per il caricamento

Le istruzioni di caricamento sono modellate in modo leggermente diverso rispetto alle altre istruzioni in nel simulatore. Per poter modellare i casi in cui la latenza del carico è incerta, le istruzioni di caricamento sono suddivise in due azioni distinte: 1) efficace il calcolo degli indirizzi e l'accesso alla memoria e 2) il write-back dei risultati. Nella questo viene fatto dividendo l'azione semantica del carico in due istruzioni separate, l'istruzione principale e un'istruzione secondaria. Inoltre, quando specifichiamo gli operandi, dobbiamo specificarli sia per l'elemento principale child. Per farlo, la specifica dell'operando viene considerata di terzine. La sintassi è:

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

Per tutte le istruzioni di caricamento viene usato il formato I-Type, così come molti dei precedenti istruzioni:

31/20 19/15 14..12 11..7 6,0
12 5 3 5 7
imm12 rs1 func3 ° opcode, codice operativo

La specifica dell'operando divide gli operandi necessari per calcolare l'indirizzo e avvia l'accesso alla memoria dalla destinazione del registro per i dati di caricamento: {( : rs1, imm12 : ), ( : : rd) }.

Poiché l'azione semantica è divisa in due istruzioni, le funzioni semantiche allo stesso modo, devi specificare due chiamabili. Per lw (carica parola), corrisponde a scritto:

    semfunc: "&RV32ILw", "&RV32ILwChild"

Le specifiche di smontaggio sono più convenzionali. Non viene menzionato il istruzione secondaria. Per lw, il valore deve essere:

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

Di seguito sono riportate le istruzioni di caricamento da implementare:

  • lb: byte di caricamento.
  • lbu: byte di caricamento non firmati.
  • lh - Carica la mezza parola.
  • lhu: carica la mezza parola non firmata.
  • lw - Carica parola.

Apporta le modifiche e crea. Controlla l'output generato. Soltanto Come in precedenza, puoi verificare il tuo lavoro riscv32i.isa e rv32i_instructions.h.

Grazie per essere arrivato fin qui. Ci auguriamo che queste informazioni ti siano state utili.