Tutorial sul decodificatore di istruzioni binarie

Gli obiettivi di questo tutorial sono:

  • Scopri la struttura e la sintassi del file di descrizione del formato binario.
  • Scopri come la descrizione del formato binario corrisponde alla descrizione ISA.
  • Scrivi le descrizioni binarie per il sottoinsieme RiscV RV32I di istruzioni.

Panoramica

Codifica dell'istruzione binaria RiscV

La codifica delle istruzioni binarie è il metodo standard per codificare le istruzioni per: su un microprocessore. Di solito sono archiviati in un file eseguibile, di solito nel formato ELF. Le istruzioni possono essere a larghezza fissa o variabili standard.

In genere, le istruzioni utilizzano un piccolo insieme di formati di codifica, con ogni formato personalizzate per il tipo di istruzioni codificate. Ad esempio, registry-register le istruzioni possono usare un unico formato che massimizza il numero di codici operativi disponibili, mentre le istruzioni di registrazione immediata ne usano un'altra che bilancia il numero disponibili per aumentare la dimensione dell'immediato che può essere codificato. Le istruzioni per rami e salti utilizzano quasi sempre formati che massimizzano le dimensioni nell'immediato in modo da supportare rami con offset più grandi.

I formati delle istruzioni utilizzati dalle istruzioni che vogliamo decodificare nel nostro RiscV sono le seguenti:

Formato R-Type, utilizzato per le istruzioni di registro:

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

Formato I-Type, utilizzato per le istruzioni di registro immediate, le istruzioni di caricamento e l'istruzione jalr, immediatamente a 12 bit.

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

Formato I-Type specializzato, utilizzato per i turni con istruzioni immediate, a 5 bit immediato:

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

Formato tipo U, utilizzato per istruzioni lunghe e immediate (lui, auipc), 20 bit immediato:

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

Formato B-Type, utilizzato per i rami condizionali, immediato a 12 bit.

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

Formato J-Type, utilizzato per l'istruzione jal, immediato a 20 bit.

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

Formato S-Type, utilizzato per le istruzioni dello store, immediato a 12 bit.

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 puoi vedere da questi formati, tutte queste istruzioni sono a 32 bit e il basso a 7 bit in ogni formato è il campo opcode. Nota inoltre che, anche se diversi formati hanno immediate dimensioni, i loro bit vengono presi parti diverse dell'istruzione. Come vedremo, il decoder binario formato delle specifiche è in grado di esprimerlo.

Descrizione codifica binaria

La codifica binaria dell'istruzione è espressa in formato binario. (.bin_fmt) file di descrizione. Descrive la codifica binaria istruzioni in un ISA in modo che un decoder di istruzioni in formato binario generati. Il decoder generato determina l'opcode, estrae il valore di operando e campi immediati, al fine di fornire le informazioni richieste dall'ISA decoder indipendente dalla codifica descritto nel tutorial precedente.

In questo tutorial scriveremo un file di descrizione della codifica binaria per un sottoinsieme delle istruzioni RiscV32I necessarie per simulare le istruzioni utilizzate in "Hello World" piccolo . Per maggiori dettagli sull'ISA di RiscV, consulta Specifiche Risc-V{.external}.

Per iniziare, apri il file: riscv_bin_decoder/riscv32i.bin_fmt.

I contenuti del file sono suddivisi in diverse sezioni.

Innanzitutto, la definizione di decoder.

decoder RiscV32I {
  // The namespace in which code will be generated.
  namespace mpact::sim::codelab;
  // The name (including any namespace qualifiers) of the opcode enum type.
  opcode_enum = "OpcodeEnum";
  // Include files specific to this decoder.
  includes {
    #include "riscv_isa_decoder/solution/riscv32i_decoder.h"
  }
  // Instruction groups for which to generate decode functions.
  RiscVInst32;
};

La definizione del decoder specifica il nome del decoder RiscV32I, così come altre quattro informazioni. Il primo è namespace e definisce lo spazio dei nomi in cui verrà inserito il codice generato. In secondo luogo, opcode_enum, che indica il modo in cui viene generato il tipo di enumerazione opcode dal decoder ISA deve essere fatto riferimento all'interno del codice generato. In terzo luogo, includes {} specifica di includere i file necessari per il codice generato questo decoder. Nel nostro caso, si tratta del file che viene prodotto dal decoder ISA dalla tutorial precedente. Puoi specificare altri file di inclusione in un campo includes {} con ambito globale definizione di Kubernetes. Questo è utile se vengono definiti più decoder e tutti devono per includere alcuni degli stessi file. La quarta è un elenco di nomi di gruppi che compongono le istruzioni per cui viene generato il decoder. Nel nostro caso ce ne sia solo uno: RiscVInst32.

Ci sono poi tre definizioni del formato. Queste rappresentano istruzioni diverse formati per una parola di istruzione a 32 bit utilizzata dalle istruzioni già definite nel file.

// The generic RiscV 32 bit instruction format.
format Inst32Format[32] {
  fields:
    unsigned bits[25];
    unsigned opcode[7];
};

// RiscV 32 bit instruction format used by a number of instructions
// needing a 12 bit immediate, including CSR instructions.
format IType[32] : Inst32Format {
  fields:
    signed imm12[12];
    unsigned rs1[5];
    unsigned func3[3];
    unsigned rd[5];
    unsigned opcode[7];
};

// RiscV instruction format used by fence instructions.
format Fence[32] : Inst32Format {
  fields:
    unsigned fm[4];
    unsigned pred[4];
    unsigned succ[4];
    unsigned rs1[5];
    unsigned func3[3];
    unsigned rd[5];
    unsigned opcode[7];
};

Il primo definisce un formato di istruzione a 32 bit denominato Inst32Format che ha due campi: bits (a 25 bit) e opcode (a 7 bit di larghezza). Ogni campo unsigned, il che significa che il valore sarà esteso a zero al momento dell'estrazione e posizionati in un tipo intero C++. La somma delle larghezze dei campi di bit deve uguale alla larghezza del formato. Lo strumento genera un errore se viene rilevato disaccordi. Questo formato non deriva da nessun altro formato, quindi considerata un formato di primo livello.

Il secondo definisce un formato di istruzione a 32 bit denominato IType che ricava da Inst32Format, rendendo questi due formati correlati. Il formato contiene 5 campi: imm12, rs1, func3, rd e opcode. Il campo imm12 è signed, il che significa che il valore sarà esteso per segno quando è estratte e inserite in un tipo intero C++. Tieni presente che IType.opcode ha entrambi lo stesso attributo firmato e non firmato e si riferisce agli stessi bit di parola di istruzione come Inst32Format.opcode.

Il terzo è un formato personalizzato utilizzato esclusivamente dall'fence istruzione, ovvero un'istruzione già specificata e al momento di cui preoccuparsi in questo tutorial.

Punto chiave: riutilizza i nomi dei campi in diversi formati correlati, a condizione che rappresentano gli stessi bit e hanno lo stesso attributo firmato/non firmato.

Dopo le definizioni del formato in riscv32i.bin_fmt arriva un gruppo di istruzioni definizione di Kubernetes. Tutte le istruzioni di un gruppo di istruzioni devono avere lo stesso di bit di bit e utilizzare un formato che derivi (magari indirettamente) dalla stessa formato di istruzione di primo livello. Quando un ISA può avere istruzioni con diverse lunghezze, viene utilizzato un gruppo di istruzioni diverso per ogni lunghezza. Inoltre, se la decodifica ISA target dipende da una modalità di esecuzione, come Arm vs. Thumb istruzioni, è necessario un gruppo di istruzioni separato per ogni modalità. La Il parser bin_fmt genera un decoder binario per ogni gruppo di istruzioni.

instruction group RiscV32I[32] "OpcodeEnum" : Inst32Format {
  fence   : Fence  : func3 == 0b000, opcode == 0b000'1111;
  csrs    : IType  : func3 == 0b010, rs1 != 0, opcode == 0b111'0011;
  csrw_nr : IType  : func3 == 0b001, rd == 0,  opcode == 0b111'0011;
  csrs_nw : IType  : func3 == 0b010, rs1 == 0, opcode == 0b111'0011;
};

Il gruppo di istruzioni definisce un nome RiscV32I, una larghezza [32], il nome il tipo di enumerazione opcode per utilizzare "OpcodeEnum" e un'istruzione di base formato. Il tipo di enumerazione del codice operativo deve essere lo stesso prodotto dalla formato da un decoder di istruzioni indipendente descritto nel tutorial sulla decodificatore.

La descrizione della codifica di ogni istruzione è composta da 3 parti:

  • Il nome del codice operativo, che deve essere lo stesso utilizzato nell'istruzione del decoder in modo che interagiscano.
  • Il formato dell'istruzione da utilizzare per l'opcode. Questo è il formato usato per soddisfare i riferimenti ai campi di bit nella parte finale.
  • Un elenco separato da virgole di vincoli di campo di bit, ==, !=, <, <=, >, e >= che tutti i valori siano veri affinché l'opcode possa corrispondere correttamente al parola di istruzione.

Il parser .bin_fmt utilizza tutte queste informazioni per creare un decoder che:

  • Fornisce funzioni di estrazione (firmato/non firmato) in base alle esigenze di ogni bit campo in ogni formato. Le funzioni di estrazione sono inserite negli spazi dei nomi denominato in base alla versione snake-case del nome del formato. Ad esempio, le funzioni di estrazione per il formato IType vengono inserite nello spazio dei nomi i_type. Ogni funzione di estrazione è dichiarata inline, prende il valore uint_t che contiene la larghezza del formato e restituisce il valore int_t più stretto (per il tipo con firma), uint_t (per il tipo non firmato), che contiene il campo estratto standard. ad esempio:
inline uint8_t ExtractOpcode(uint32_t value) {
  return value & 0x7f;
}
  • Una funzione di decodifica per ogni gruppo di istruzioni. Restituisce un valore di tipo OpcodeEnum e prende il tipo uint_t più stretto che contiene la larghezza di il formato del gruppo di istruzioni.

Esegui build iniziale

Cambia la directory in riscv_bin_decoder e crea il progetto utilizzando seguente comando:

$ cd riscv_bin_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_bin_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_bin_decoder
  • riscv32i_bin_decoder.h
  • riscv32i_bin_decoder.cc

Il file di intestazione generato (.h)

Apri riscv32i_bin_decoder.h. La prima parte del file contiene file boilerplate, dichiarazioni dello spazio dei nomi e file inclusi. In seguito c'è una funzione helper basata su modelli nello spazio dei nomi internal. Questa funzione viene utilizzato per estrarre campi di bit da formati troppo lunghi per adattarsi a un file C++ a 64 bit. numero intero.

#ifndef RISCV32I_BIN_DECODER_H
#define RISCV32I_BIN_DECODER_H

#include <iostream>
#include <cstdint>

#include "third_party/absl/functional/any_invocable.h"


#include "learning/brain/research/mpact/sim/codelab/riscv_isa_decoder/solution/riscv32i_decoder.h"

namespace mpact {
namespace sim {
namespace codelab {


namespace internal {

template <typename T>
static inline T ExtractBits(const uint8_t *data, int data_size,
                            int bit_index, int width) {
  if (width == 0) return 0;

  int byte_pos = bit_index >> 3;
  int end_byte = (bit_index + width - 1) >> 3;
  int start_bit = bit_index & 0x7;

  // If it is only from one byte, extract and return.
  if (byte_pos == end_byte) {
    uint8_t mask = 0xff >> start_bit;
    return (mask & data[byte_pos]) >> (8 - start_bit - width);
  }

  // Extract from the first byte.
  T val = 0;
  val = data[byte_pos++] & 0xff >> start_bit;
  int remainder = width - (8 - start_bit);
  while (remainder >= 8) {
    val = (val << 8) | data[byte_pos++];
    remainder -= 8;
  }

  // Extract any remaining bits.
  if (remainder > 0) {
    val <<= remainder;
    int shift = 8 - remainder;
    uint8_t mask = 0b1111'1111 << shift;
    val |= (data[byte_pos] & mask) >> shift;
  }
  return val;
}

}  // namespace internal

Dopo la sezione iniziale c'è un insieme di tre spazi dei nomi, uno per ogni delle dichiarazioni format nel file riscv32i.bin_fmt:


namespace fence {

...

}  // namespace fence

namespace i_type {

...

}  // namespace i_type

namespace inst32_format {

...

}  // namespace inst32_format

All'interno di ciascuno di questi spazi dei nomi, la funzione di estrazione del campo di bit inline per ogni campo di bit in quel formato. Inoltre, il formato di base duplica funzioni di estrazione dai formati discendenti per cui 1) i nomi dei campi sono presenti solo in un nome di campo singolo oppure 2) per il quale le i nomi dei campi fanno riferimento allo stesso campo del tipo (con firma/non firmato e posizioni bit) in tutti i formati in cui si presentano. In questo modo vengono abilitati i campi di bit che descrivono bit da estrarre utilizzando le funzioni nello spazio dei nomi del formato di primo livello.

Di seguito sono mostrate le funzioni nello spazio dei nomi i_type:

namespace i_type {

inline uint8_t ExtractFunc3(uint32_t value) {
  return  (value >> 12) & 0x7;
}

inline int16_t ExtractImm12(uint32_t value) {
  int16_t result = ( (value >> 20) & 0xfff) << 4;
  result = result >> 4;
  return result;
}

inline uint8_t ExtractOpcode(uint32_t value) {
  return value & 0x7f;
}

inline uint8_t ExtractRd(uint32_t value) {
  return  (value >> 7) & 0x1f;
}

inline uint8_t ExtractRs1(uint32_t value) {
  return  (value >> 15) & 0x1f;
}

}  // namespace i_type

Infine, la dichiarazione della funzione decoder per l'istruzione il gruppo RiscVInst32 è stato dichiarato. Prende un token non firmato a 32 bit come valore la parola di istruzione e restituisce il membro della classe di enumerazione OpcodeEnum corrisponde a quello specificato o OpcodeEnum::kNone se non ci sono corrispondenze.

OpcodeEnum DecodeRiscVInst32(uint32_t inst_word);

Il file di origine generato (.cc)

Ora apri riscv32i_bin_decoder.cc. La prima parte del file contiene le dichiarazioni #include e dello spazio dei nomi, seguite dalla funzione decoder dichiarazioni:

#include "riscv32i_bin_decoder.h"

namespace mpact {
namespace sim {
namespace codelab {

OpcodeEnum DecodeRiscVInst32None(uint32_t);
OpcodeEnum DecodeRiscVInst32_0(uint32_t inst_word);
OpcodeEnum DecodeRiscVInst32_0_3(uint32_t inst_word);
OpcodeEnum DecodeRiscVInst32_0_3c(uint32_t inst_word);
OpcodeEnum DecodeRiscVInst32_0_5c(uint32_t inst_word);

DecodeRiscVInst32None viene utilizzato per le azioni di decodifica vuote, ad esempio quelle che restituiscono OpcodeEnum::kNone. Le altre tre funzioni costituiscono decoder generato. Il decoder complessivo funziona in modo gerarchico. Un insieme di bit nella parola di istruzione viene calcolato istruzioni o gruppi di istruzioni al livello superiore. I bit non devono essere contigui. Il numero di bit determina la dimensione di una tabella di ricerca che viene compilata con funzioni di decoder di secondo livello. come vedrai nel prossimo sezione del file:

absl::AnyInvocable<OpcodeEnum(uint32_t)> parse_group_RiscVInst32_0[kParseGroupRiscVInst32_0_Size] = {
    &DecodeRiscVInst32None, &DecodeRiscVInst32None,
    &DecodeRiscVInst32None, &DecodeRiscVInst32_0_3,
    &DecodeRiscVInst32None, &DecodeRiscVInst32None,

    ...

    &DecodeRiscVInst32None, &DecodeRiscVInst32None,
    &DecodeRiscVInst32None, &DecodeRiscVInst32None,
    &DecodeRiscVInst32_0_3c, &DecodeRiscVInst32None,

    ...
};

Infine, le funzioni di decoder sono definite:

OpcodeEnum DecodeRiscVInst32None(uint32_t) {
  return OpcodeEnum::kNone;
}

OpcodeEnum DecodeRiscVInst32_0(uint32_t inst_word) {
  if ((inst_word & 0x4003) != 0x3) return OpcodeEnum::kNone;
  uint32_t index;
  index = (inst_word >> 2) & 0x1f;
  index |= (inst_word >> 7) & 0x60;
  return parse_group_RiscVInst32_0[index](inst_word);
}

OpcodeEnum DecodeRiscVInst32_0_3(uint32_t inst_word) {
  return OpcodeEnum::kFence;
}

OpcodeEnum DecodeRiscVInst32_0_3c(uint32_t inst_word) {
  if ((inst_word & 0xf80) != 0x0) return OpcodeEnum::kNone;
  return OpcodeEnum::kCsrwNr;
}

OpcodeEnum DecodeRiscVInst32_0_5c(uint32_t inst_word) {
  uint32_t rs1_value = (inst_word >> 15) & 0x1f;
  if (rs1_value != 0x0)
    return OpcodeEnum::kCsrs;
  if (rs1_value == 0x0)
    return OpcodeEnum::kCsrsNw;
  return OpcodeEnum::kNone;
}

OpcodeEnum DecodeRiscVInst32(uint32_t inst_word) {
  OpcodeEnum opcode;
  opcode = DecodeRiscVInst32_0(inst_word);
  return opcode;
}

In questo caso, quando sono definite solo 4 istruzioni, c'è solo un un singolo livello di decodifica e una tabella di ricerca molto sparsa. Poiché le istruzioni sono aggiunto, la struttura del decoder cambierà e il numero di livelli in decoder possono aumentare.


Aggiungi istruzioni ALU per la registrazione e la registrazione

Ora è il momento di aggiungere alcune nuove istruzioni al file riscv32i.bin_fmt. La Il primo gruppo di istruzioni è costituito dalle istruzioni ALU registrate nel registro, come add, and e così via. Su RiscV32, queste utilizzano tutte l'istruzione binaria di tipo R formato:

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

La prima cosa da fare è aggiungere il formato. Vai avanti e apri riscv32i.bin_fmt nel tuo editor preferito. Subito dopo Inst32Format aggiungi un formato denominato RType che deriva da Inst32Format. Tutti i campi di bit in RType sono unsigned. Utilizza nomi, larghezza in bit e ordine (da sinistra a destra) dalla tabella precedente per definire il formato. Se hai bisogno di un suggerimento o vuoi vedere la soluzione completa, fai clic qui.

Poi dobbiamo aggiungere le istruzioni. Ecco le istruzioni:

  • 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.

Le loro codifiche sono:

31,25 24/20 19/15 14..12 11..7 6,0 nome opcode
000 0000 rs2 rs1 000 ° 011 0011 add
000 0000 rs2 rs1 111 ° 011 0011 e
000 0000 rs2 rs1 110 ° 011 0011 o
000 0000 rs2 rs1 001 ° 011 0011 sll
000 0000 rs2 rs1 011 ° 011 0011 sltu
010 0000 rs2 rs1 000 ° 011 0011 Pub/Sub.
000 0000 rs2 rs1 100 ° 011 0011 Xor
func7 func3 opcode, codice operativo

Aggiungi queste definizioni di istruzione prima delle altre istruzioni nella sezione Gruppo di istruzioni RiscVInst32. Le stringhe binarie sono specificate con prefisso di 0b (simile a 0x per i numeri esadecimali). Per semplificare la leggere lunghe stringhe di cifre binarie, puoi anche inserire l'apice singola ' come un separatore di cifre che ritieni adeguato.

Ognuna di queste definizioni di istruzione avrà tre vincoli, ovvero func7, func3 e opcode. Per tutti tranne sub, il vincolo func7 potrebbe essere:

func7 == 0b000'0000

Il vincolo func3 varia per la maggior parte delle istruzioni. Per add e sub è:

func3 == 0b000

Il vincolo opcode è lo stesso per ciascuna di queste istruzioni:

opcode == 0b011'0011

Ricorda di terminare ogni riga con un punto e virgola ;.

La soluzione completa è qui

A questo punto, crea il tuo progetto come in precedenza e apri lo strumento riscv32i_bin_decoder.cc. Possiamo vedere che funzioni decoder aggiuntive generati per gestire le nuove istruzioni. Per la maggior parte sono simili a quelli generati in precedenza, ma guarda DecodeRiscVInst32_0_c utilizzato per la decodifica add/sub:

OpcodeEnum DecodeRiscVInst32_0_c(uint32_t inst_word) {
  static constexpr OpcodeEnum opcodes[2] = {
    OpcodeEnum::kAdd,
    OpcodeEnum::kSub,
  };
  if ((inst_word & 0xbe000000) != 0x0) return OpcodeEnum::kNone;
  uint32_t index;
  index = (inst_word >> 30) & 0x1;
  return opcodes[index];
}

In questa funzione viene generata una tabella di decodifica statica e viene generato un valore di ricerca estratti dalla parola di istruzione per selezionare l'indice appropriato. Questa operazione aggiunge secondo livello nella gerarchia del decoder delle istruzioni, ma poiché l'opcode può essere cercato direttamente in una tabella senza ulteriori confronti, è incorporato in anziché richiedere un'altra chiamata di funzione.


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

Il formato I-Type esiste già in riscv32i.bin_fmt, quindi non è necessario aggiungi quel formato.

Se confrontiamo il formato I-Type specializzato con il formato R-Type definito in nell'esercizio precedente, vediamo che l'unica differenza è che i campi rs2 è stato rinominato in uimm5. Invece di aggiungere un nuovo formato, possiamo ampliare formato R-Type. Non possiamo aggiungere un altro campo, perché verrebbe aumentato la larghezza del il formato, ma possiamo aggiungere un overlay. Un overlay è l'alias di un insieme di bit nel formato e può essere utilizzato per combinare più sottosequenze in un'entità con nome separata. L'effetto collaterale è che il codice generato ora includerà anche una funzione di estrazione per l'overlay, oltre a per i campi. In questo caso, quando rs2 e uimm5 non sono firmati, non fa molta differenza se non per chiarire che il campo viene utilizzato nell'immediato. Per aggiungere un overlay denominato uimm5 al formato R-Type, aggiungi dopo l'ultimo campo:

  overlays:
    unsigned uimm5[5] = rs2;

L'unico nuovo formato che dobbiamo aggiungere è il formato U-Type. Prima di aggiungere formato, consideriamo le due istruzioni che utilizzano tale formato: auipc e lui. Entrambe spostano il valore immediato a 20 bit lasciato di 12 prima di usarlo aggiungere il pc (auipc) o scriverlo direttamente in un registro (lui). Usando un overlay possiamo fornire una versione pre-spostamento del testo immediato, di spostare un po' di calcolo dall'esecuzione dell'istruzione all'istruzione decodificare. Innanzitutto aggiungi il formato in base ai campi specificati nella tabella in alto. Poi possiamo aggiungere il seguente overlay:

  overlays:
    unsigned uimm32[32] = uimm20, 0b0000'0000'0000;

La sintassi dell'overlay ci permette di concatenare non solo i campi, ma i valori letterali beh. In questo caso la concateniamo con 12 zeri, spostandola quindi verso sinistra per 12.

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 loro codifiche sono:

31/20 19/15 14..12 11..7 6,0 opcode_name
imm12 rs1 000 ° 001 0011 Addi
imm12 rs1 111 ° 001 0011 Andi
imm12 rs1 110 ° 001 0011 Ori
imm12 rs1 100 ° 001 0011 Xori
func3 opcode, codice operativo

Le istruzioni R-Type (specializzato I-Type) 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 loro codifiche sono:

31,25 24/20 19/15 14..12 11..7 6,0 nome opcode
000 0000 uimm5 rs1 001 ° 001 0011 SLL
010 0000 uimm5 rs1 101 ° 001 0011 Sara
000 0000 uimm5 rs1 101 ° 001 0011 srli
func7 func3 opcode, codice operativo

Le istruzioni per il tipo U che dobbiamo aggiungere sono:

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

Le loro codifiche sono:

31.12 11..7 6,0 nome opcode
uimm20 ° 001 0111 auipc
uimm20 ° 011 0111 lui
opcode, codice operativo

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


La serie successiva di istruzioni da definire è il ramo condizionale istruzioni, le istruzioni jump-and-link e il registro jump-and-link istruzioni.

Tutti i rami condizionali che stiamo aggiungendo utilizzano la codifica B-Type.

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

Mentre la codifica B-Type è identica nel layout alla codifica R-Type, scegli di usare un nuovo tipo di formato in modo che sia in linea con la documentazione di RiscV. Potresti anche aver aggiunto un overlay per ottenere il ramo appropriato dello spostamento immediato, usando i campi func7 e rd del parametro R-Type codifica.

È necessario aggiungere un formato BType con i campi come specificato sopra, ma non sufficienti. Come puoi vedere, le istruzioni immediate sono suddivise in due campi di istruzione. Inoltre, le istruzioni per i rami non lo considerano una semplice concatenazione di nei due campi. Ogni campo è invece ulteriormente partizionato e queste partizioni sono concatenati in un ordine diverso. Infine, questo valore viene spostato a sinistra uno per ottenere un offset allineato a 16 bit.

La sequenza di bit nella parola di istruzione utilizzata per formare l'immediato sono: 31, 7, 30..25, 11..8. Ciò corrisponde ai seguenti riferimenti di campi secondari, l'indice o l'intervallo specifica i bit nel campo, numerati da destra a sinistra, ovvero imm7[6] si riferisce alla metrica msb di imm7 e imm5[0] alla lsb di imm5.

imm7[6], imm5[0], imm7[5..0], imm5[4..1]

Rendere questa manipolazione dei bit parte integrante delle istruzioni del ramo di per sé ha due grandi svantaggi. Innanzitutto, collega l'implementazione della funzione semantica a nella rappresentazione dell'istruzione binaria. In secondo luogo, aggiunge più tempo di esecuzione overhead. La risposta è aggiungere un overlay al formato BType, includendo "0" finale per tenere conto del turno di sinistra.

  overlays:
    signed b_imm[13] = imm7[6], imm5[0], imm7[5..0], imm5[4..1], 0b0;

Nota che l'overlay è firmato, quindi verrà esteso automaticamente. quando viene estratto dalla parola di istruzione.

L'istruzione jump-and-link (immediata) utilizza la codifica J-Type:

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

Anche questo è un formato facile da aggiungere, ma ancora una volta, l'immediato utilizzato l'istruzione non è così semplice come sembra. Le sequenze di bit utilizzate l'immediato completo sono: 31, 19..12, 20, 30..21, e l'immediato finale è spostato a sinistra di uno per l'allineamento a metà parola. La soluzione consiste nell'aggiungere un altro overlay (21 bit per tenere conto dello spostamento sinistro) al formato:

  overlays:
    signed j_imm[21] = imm20[19, 7..0, 8, 18..9], 0b0;

Come puoi vedere, la sintassi per gli overlay supporta la specifica di più intervalli in un in un formato breve. Inoltre, se non viene utilizzato alcun nome di campo, il bit numeri fanno riferimento alla parola di istruzione stessa, quindi quanto sopra potrebbe essere scritto come:

    signed j_imm[21] = [31, 19..12, 20, 30..21], 0b0;

Infine, il jump-and-link (registro) utilizza il formato I-type usato in precedenza.

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

Questa volta, non è necessario apportare modifiche al formato.

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.

Sono codificati nel seguente modo:

31,25 24/20 19/15 14..12 11..7 6,0 nome opcode
imm7 rs2 rs1 000 imm5 110 0011 beq
imm7 rs2 rs1 101 imm5 110 0011 bge
imm7 rs2 rs1 111 imm5 110 0011 Bgeu
imm7 rs2 rs1 100 imm5 110 0011 blt
imm7 rs2 rs1 110 imm5 110 0011 bltu
imm7 rs2 rs1 001 imm5 110 0011 bne
func3 opcode, codice operativo

L'istruzione jal è codificata come segue:

31.12 11..7 6,0 nome opcode
imm20 ° 110 x 1111 Jal
opcode, codice operativo

L'istruzione jalr è codificata come segue:

31/20 19/15 14..12 11..7 6,0 opcode_name
imm12 rs1 000 ° 110 0111 Jalr
func3 opcode, codice operativo

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


Aggiungi istruzioni per il negozio

Le istruzioni dello store utilizzano la codifica S-Type, che è identica alla codifica B-Type utilizzata dalle istruzioni di ramo, ad eccezione della composizione del nell'immediato. Scegliamo di aggiungere il formato SType per rimanere in linea con il RiscV documentazione.

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

Nel caso del formato SType, per fortuna l'immediato è una risposta concatenazione in avanti dei due campi immediati, quindi la specifica dell'overlay è semplicemente:

  overlays:
    signed s_imm[12] = imm7, imm5;

Tieni presente che non sono necessari specificatori di intervallo di bit quando concatenano interi campi.

Le istruzioni dello store sono codificate come segue:

31,25 24/20 19/15 14..12 11..7 6,0 nome opcode
imm7 rs2 rs1 000 imm5 010 0011 sb
imm7 rs2 rs1 001 imm5 010 0011 s
imm7 rs2 rs1 010 imm5 010 0011 sw
func3 opcode, codice operativo

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


Aggiungi istruzioni per il caricamento

Le istruzioni di caricamento utilizzano il formato I-Type. Non è necessario apportare modifiche.

Le codifiche sono:

31/20 19/15 14..12 11..7 6,0 opcode_name
imm12 rs1 000 ° 000 0011 lb
imm12 rs1 100 ° 000 0011 lbu
imm12 rs1 001 ° 000 0011 sx
imm12 rs1 101 ° 000 0011 lhu
imm12 rs1 010 ° 000 0011 lw
func3 opcode, codice operativo

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

Il tutorial termina qui, ci auguriamo che ti sia stato utile.