Gli obiettivi di questo tutorial sono:
- Scopri come l'ISA generato e i decoder binari si integrano tra loro.
- Scrivere il codice C++ necessario per creare un decoder di istruzioni completo per RiscV RV32I che combina ISA e decoder binari.
Comprendere il decoder di istruzioni
Il decoder di istruzioni è responsabile, dato un indirizzo di istruzione,
la parola di istruzione dalla memoria e restituire un'istanza
Instruction
che rappresenta l'istruzione.
Il decoder di primo livello implementa il generic::DecoderInterface
mostrato di seguito:
// This is the simulator's interface to the instruction decoder.
class DecoderInterface {
public:
// Return a decoded instruction for the given address. If there are errors
// in the instruciton decoding, the decoder should still produce an
// instruction that can be executed, but its semantic action function should
// set an error condition in the simulation when executed.
virtual Instruction *DecodeInstruction(uint64_t address) = 0;
virtual ~DecoderInterface() = default;
};
Come puoi vedere, esiste un solo metodo da implementare: cpp
virtual Instruction *DecodeInstruction(uint64_t address);
Ora vediamo cosa viene fornito e cosa serve dal codice generato.
Considera innanzitutto la classe di primo livello RiscV32IInstructionSet
nel file
riscv32i_decoder.h
, generato alla fine del tutorial nella
decodificatore ISA. Per visualizzare nuovamente i contenuti, vai alla directory della soluzione di
quel tutorial e ricreare il tutto.
$ cd riscv_isa_decoder/solution
$ bazel build :all
...<snip>...
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
Vengono visualizzati i quattro file sorgente che contengono il codice C++ generato:
riscv32i_decoder.h
riscv32i_decoder.cc
riscv32i_enums.h
riscv32i_enums.cc
Apri il primo file riscv32i_decoder.h
. Esistono tre classi
dare un'occhiata a:
RiscV32IEncodingBase
RiscV32IInstructionSetFactory
RiscV32IInstructionSet
Prendi nota della denominazione delle classi. Il nome di tutti i corsi si basa sul
Versione pascal del nome nella "isa" dichiarazione in tale file:
isa RiscV32I { ... }
Iniziamo prima dal corso RiscVIInstructionSet
. Viene mostrato di seguito:
class RiscV32IInstructionSet {
public:
RiscV32IInstructionSet(ArchState *arch_state,
RiscV32IInstructionSetFactory *factory);
Instruction *Decode(uint64 address, RiscV32IEncodingBase *encoding);
private:
std::unique_ptr<Riscv32Slot> riscv32_decoder_;
ArchState *arch_state_;
};
Non esistono metodi virtuali in questa classe, quindi è una classe autonoma, ma
due cose. Innanzitutto, il costruttore prende un puntatore a un'istanza
RiscV32IInstructionSetFactory
corso. Si tratta di una classe che ha generato
decoder usa per creare un'istanza della classe RiscV32Slot
, che viene utilizzata
decodificare tutte le istruzioni definite per slot RiscV32
come definito in
riscv32i.isa
file. In secondo luogo, il metodo Decode
richiede un parametro aggiuntivo
di tipo puntatore a RiscV32IEncodingBase
, questa è una classe che fornirà
tra il decoder isa generato nel primo tutorial e il file binario
decoder generato nel secondo lab.
La classe RiscV32IInstructionSetFactory
è una classe astratta da cui
ricavare la nostra implementazione per il decoder completo. Nella maggior parte dei casi,
è banale: indica semplicemente un metodo per chiamare il costruttore per ogni
slot definita nel nostro file .isa
. Nel nostro caso, è molto semplice perché
è solo una singola classe di questo tipo: Riscv32Slot
(il caso pascal del nome riscv32
concatenata con Slot
). Il metodo non viene generato automaticamente perché
alcuni casi d'uso avanzati in cui potrebbe esserci utilità nel ricavare una sottoclasse
dallo slot, chiamando il relativo costruttore.
Esamineremo l'ultimo corso RiscV32IEncodingBase
più avanti
tutorial, in quanto è l'oggetto di un altro esercizio.
Definisci decoder di istruzioni di primo livello
Definisci la classe di fabbrica
Se hai ricreato il progetto per il primo tutorial, assicurati di tornare a
nella directory riscv_full_decoder
.
Apri il file riscv32_decoder.h
. Tutti i file di inclusione necessari contengono
è già stato aggiunto e gli spazi dei nomi sono stati configurati.
Dopo il commento contrassegnato come //Exercise 1 - step 1
, definisci il corso
RiscV32IsaFactory
che eredita da RiscV32IInstructionSetFactory
.
class RiscV32IsaFactory : public RiscV32InstructionSetFactory {};
Definisci quindi l'override per CreateRiscv32Slot
. Poiché non utilizziamo nessuna
classi derivate di Riscv32Slot
, è sufficiente allocare una nuova istanza utilizzando
std::make_unique
.
std::unique_ptr<Riscv32Slot> CreateRiscv32Slot(ArchState *) override {
return std::make_unique<Riscv32Slot>(state);
}
Se hai bisogno di aiuto (o vuoi controllare il tuo lavoro), la risposta completa è qui
Definisci la classe decoder
Dichiarazioni di costruttori, distruttori e metodi
A questo punto occorre definire la classe decoder. Nello stesso file di cui sopra, vai alla sezione
dichiarazione di RiscV32Decoder
. Espandi la dichiarazione in una definizione di classe
dove RiscV32Decoder
eredita da generic::DecoderInterface
.
class RiscV32Decoder : public generic::DecoderInterface {
public:
};
Prima di scrivere il costruttore, diamo una rapida occhiata al codice
nel nostro secondo tutorial sul decoder binario. Oltre a tutte le
Extract
funzioni, ecco la funzione DecodeRiscVInst32
:
OpcodeEnum DecodeRiscVInst32(uint32_t inst_word);
Questa funzione prende la parola di istruzione che deve essere decodificata e restituisce
il codice operativo che corrisponde all'istruzione. D'altra parte,
DecodeInterface
classe che RiscV32Decoder
implementa solo i passaggi in una
. Pertanto, la classe RiscV32Decoder
deve poter accedere alla memoria
leggi la parola di istruzione da passare a DecodeRiscVInst32()
. In questo progetto
accedere alla memoria tramite una semplice interfaccia di memoria definita
.../mpact/sim/util/memory
denominato util::MemoryInterface
, di seguito:
// Load data from address into the DataBuffer, then schedule the Instruction
// inst (if not nullptr) to be executed (using the function delay line) with
// context. The size of the data access is based on size of the data buffer.
virtual void Load(uint64_t address, DataBuffer *db, Instruction *inst,
ReferenceCount *context) = 0;
Inoltre, dobbiamo poter passare un'istanza della classe state
alla
costruttori delle altre classi decoder. La classe di stato appropriata è
Classe riscv::RiscVState
, che deriva da generic::ArchState
, con aggiunta
per RiscV. Ciò significa che dobbiamo dichiarare il costruttore
può portare un puntatore al state
e al memory
:
RiscV32Decoder(riscv::RiscVState *state, util::MemoryInterface *memory);
Elimina il costruttore predefinito e sostituisci il distruttore:
RiscV32Decoder() = delete;
~RiscV32Decoder() override;
Poi dichiara il metodo DecodeInstruction
da cui eseguire l'override
generic::DecoderInterface
.
generic::Instruction *DecodeInstruction(uint64_t address) override;
Se hai bisogno di aiuto (o vuoi controllare il tuo lavoro), la risposta completa è qui
Definizioni dei membri dei dati
La classe RiscV32Decoder
avrà bisogno di membri di dati privati per archiviare
dei parametri del costruttore e un puntatore alla classe fabbrica.
private:
riscv::RiscVState *state_;
util::MemoryInterface *memory_;
Richiede anche un puntatore alla classe di codifica derivata
RiscV32IEncodingBase
, chiamiamolo RiscV32IEncoding
(implementeremo
questo nell'esercizio 2). Inoltre ha bisogno di un puntatore a un'istanza
RiscV32IInstructionSet
, quindi aggiungi:
RiscV32IsaFactory *riscv_isa_factory_;
RiscV32IEncoding *riscv_encoding_;
RiscV32IInstructionSet *riscv_isa_;
Infine, dobbiamo definire un membro di dati da utilizzare con la nostra interfaccia di memoria:
generic::DataBuffer *inst_db_;
Se hai bisogno di aiuto (o vuoi controllare il tuo lavoro), la risposta completa è qui
Definire i metodi delle classi di decodifica
A questo punto occorre implementare il costruttore, il distruttore
DecodeInstruction
. Apri il file riscv32_decoder.cc
. Il vuoto
sono già presenti nel file, così come dichiarazioni dello spazio dei nomi e un paio
di using
dichiarazioni.
Definizione di costruttore
Il costruttore deve inizializzare solo i membri dei dati. Inizializziamo
state_
e memory_
:
RiscV32Decoder::RiscV32Decoder(riscv::RiscVState *state,
util::MemoryInterface *memory)
: state_(state), memory_(memory) {
Successivamente, alloca le istanze di ciascuna delle classi relative al decoder, passando i parametri appropriati.
// Allocate the isa factory class, the top level isa decoder instance, and
// the encoding parser.
riscv_isa_factory_ = new RiscV32IsaFactory();
riscv_isa_ = new RiscV32IInstructionSet(state, riscv_isa_factory_);
riscv_encoding_ = new RiscV32IEncoding(state);
Infine, alloca l'istanza DataBuffer
. Viene allocato utilizzando un modello
accessibile tramite il membro state_
. Assegniamo le dimensioni di un buffer di dati
un singolo uint32_t
, che è la dimensione della parola di istruzione.
inst_db_ = state_->db_factory()->Allocate<uint32_t>(1);
Definizione di distruttore
Il distruttore è semplice, basta liberare gli oggetti allocati nel costruttore,
ma con un solo tocco. L'istanza del buffer di dati viene conteggiata come riferimento, quindi invece
richiamando delete
su quel puntatore, DecRef()
l'oggetto:
RiscV32Decoder::~RiscV32Decoder() {
inst_db_->DecRef();
delete riscv_isa_;
delete riscv_isa_factory_;
delete riscv_encoding_;
}
Definizione del metodo
Nel nostro caso, l'implementazione di questo metodo è piuttosto semplice. Supporremo che l'indirizzo sia allineato correttamente e che non ci siano ulteriori controlli degli errori. obbligatorio.
Innanzitutto, la parola di istruzione deve essere recuperata dalla memoria utilizzando
e l'istanza DataBuffer
.
memory_->Load(address, inst_db_, nullptr, nullptr);
uint32_t iword = inst_db_->Get<uint32_t>(0);
Quindi, chiamiamo l'istanza RiscVIEncoding
per analizzare la parola di istruzione,
che deve essere fatto prima di chiamare il decoder ISA stesso. Ricorda che l'ISA
decoder gestisce direttamente l'istanza RiscVIEncoding
per ottenere l'opcode
e operandi specificati dalla parola di istruzione. Non l'abbiamo implementato
ma usiamo void ParseInstruction(uint32_t)
come metodo.
riscv_encoding_->ParseInstruction(iword);
Infine, chiamiamo il decoder ISA, passando l'indirizzo e la classe Encoding.
auto *instruction = riscv_isa_->Decode(address, riscv_encoding_);
return instruction;
Se hai bisogno di aiuto (o vuoi controllare il tuo lavoro), la risposta completa è qui
La classe di codifica
La classe di codifica implementa un'interfaccia utilizzata dalla classe decoder per ottenere l'opcode dell'istruzione, gli operandi di origine e di destinazione degli operandi delle risorse. Questi oggetti dipendono tutti da informazioni decoder, come il codice operativo, i valori di campi specifici parola di istruzione ecc. È separato dalla classe decoder per mantenerla indipendente dalla codifica e abilitare il supporto di più schemi di codifica diversi in futuro.
RiscV32IEncodingBase
è una classe astratta. L'insieme di metodi a nostra disposizione
da implementare nella nostra classe derivata.
class RiscV32IEncodingBase {
public:
virtual ~RiscV32IEncodingBase() = default;
virtual OpcodeEnum GetOpcode(SlotEnum slot, int entry) = 0;
virtual ResourceOperandInterface *
GetSimpleResourceOperand(SlotEnum slot, int entry, OpcodeEnum opcode,
SimpleResourceVector &resource_vec, int end) = 0;
virtual ResourceOperandInterface *
GetComplexResourceOperand(SlotEnum slot, int entry, OpcodeEnum opcode,
ComplexResourceEnum resource_op,
int begin, int end) = 0;
virtual PredicateOperandInterface *
GetPredicate(SlotEnum slot, int entry, OpcodeEnum opcode,
PredOpEnum pred_op) = 0;
virtual SourceOperandInterface *
GetSource(SlotEnum slot, int entry, OpcodeEnum opcode,
SourceOpEnum source_op, int source_no) = 0;
virtual DestinationOperandInterface *
GetDestination(SlotEnum slot, int entry, OpcodeEnum opcode,
DestOpEnum dest_op, int dest_no, int latency) = 0;
virtual int GetLatency(SlotEnum slot, int entry, OpcodeEnum opcode,
DestOpEnum dest_op, int dest_no) = 0;
};
A prima vista sembra un po' complicato, in particolare per il numero di ma per un'architettura semplice come RiscV in realtà ignoriamo la maggior parte dei i parametri, in quanto i relativi valori saranno impliciti.
Vediamo i metodi uno alla volta.
OpcodeEnum GetOpcode(SlotEnum slot, int entry);
Il metodo GetOpcode
restituisce il membro OpcodeEnum
per l'attuale
istruzione, identificando il codice operativo dell'istruzione. Il corso OpcodeEnum
è
definita nel file decoder ISA generato riscv32i_enums.h
. Il metodo richiede
due parametri, entrambi
ignorabili ai nostri scopi. Il primo di
questo è il tipo di slot (una classe enum definita anche in riscv32i_enums.h
),
che, poiché RiscV ha un solo slot, ha un solo valore possibile:
SlotEnum::kRiscv32
. Il secondo è il numero di istanza dello slot (nel caso
sono presenti più istanze dello slot, che possono verificarsi in alcune
architetture di progetto).
ResourceOperandInterface *
GetSimpleResourceOperand(SlotEnum slot, int entry, OpcodeEnum opcode,
SimpleResourceVector &resource_vec, int end)
ResourceOperandInterface *
GetComplexResourceOperand(SlotEnum slot, int entry, OpcodeEnum opcode,
ComplexResourceEnum resource_op,
int begin, int end);
I due metodi successivi vengono usati per modellare le risorse hardware nel processore
per migliorare la precisione del ciclo. Per i nostri esercizi tutorial, non utilizzeremo
in modo che nell'implementazione vengano testati, restituendo nullptr
.
PredicateOperandInterface *
GetPredicate(SlotEnum slot, int entry, OpcodeEnum opcode,
PredOpEnum pred_op);
SourceOperandInterface *
GetSource(SlotEnum slot, int entry, OpcodeEnum opcode,
SourceOpEnum source_op, int source_no);
DestinationOperandInterface *
GetDestination(SlotEnum slot, int entry, OpcodeEnum opcode,
DestOpEnum dest_op, int dest_no, int latency);
Questi tre metodi restituiscono puntatori agli oggetti operando utilizzati all'interno
le funzioni semantiche dell'istruzione per accedere al valore di qualsiasi istruzione
operando del predicato, ciascuno degli operandi dell'origine dell'istruzione, e scrive nuovi
agli operandi di destinazione dell'istruzione. Poiché RiscV non utilizza
predicati di istruzione, quel metodo deve restituire solo nullptr
.
Il pattern dei parametri è simile in queste funzioni. Innanzitutto, proprio come
GetOpcode
lo slot e la voce sono stati passati. Quindi l'opcode per il
per la quale deve essere creato l'operando. Viene utilizzato solo se
codici operativi diversi devono restituire oggetti operando diversi per lo stesso operando
che non avviene in questo simulatore RiscV.
Poi c'è la voce Predicato, Origine e Destinazione, enumerazione operando
identifica l'operando da creare. Questi provengono dai tre
OpEnums in riscv32i_enums.h
come illustrato di seguito:
enum class PredOpEnum {
kNone = 0,
kPastMaxValue = 1,
};
enum class SourceOpEnum {
kNone = 0,
kBimm12 = 1,
kCsr = 2,
kImm12 = 3,
kJimm20 = 4,
kRs1 = 5,
kRs2 = 6,
kSimm12 = 7,
kUimm20 = 8,
kUimm5 = 9,
kPastMaxValue = 10,
};
enum class DestOpEnum {
kNone = 0,
kCsr = 1,
kNextPc = 2,
kRd = 3,
kPastMaxValue = 4,
};
Se torni a guardare
riscv32.isa
noti che corrispondono agli insiemi di origini e destinazioni
nomi degli operandi usati nella dichiarazione di ogni istruzione. Utilizzando diverse
nomi degli operandi per gli operandi che rappresentano diversi campi di bit e operando
semplifica la scrittura della classe di codifica in quanto membro enum univoco
determina l'esatto tipo di operando da restituire e non è necessario
considera i valori dei parametri di slot, voce o opcode.
Infine, per gli operandi di origine e di destinazione, la posizione ordinale dei l'operando viene passato (anche in questo caso possiamo ignorarlo) e per la destinazione operando, la latenza (in cicli) che intercorre tra il momento in cui l'istruzione e il risultato della destinazione è disponibile per le istruzioni successive. Nel nostro simulatore la latenza sarà pari a 0, il che significa che l'istruzione scrive il risultato immediatamente al registro.
int GetLatency(SlotEnum slot, int entry, OpcodeEnum opcode,
DestOpEnum dest_op, int dest_no);
La funzione finale viene utilizzata per ottenere la latenza di una destinazione specifica
operando se è stato specificato come *
nel file .isa
. È raro,
e non viene utilizzata per questo simulatore RiscV, quindi la nostra implementazione di questa funzione
restituirà solo 0.
Definisci la classe di codifica
File di intestazione (.h)
Metodi
Apri il file riscv32i_encoding.h
. Tutti i file di inclusione necessari contengono
è già stato aggiunto e gli spazi dei nomi sono stati configurati. L'aggiunta di tutti i codici è
ha finito di seguire il commento // Exercise 2.
Per iniziare, definiamo una classe RiscV32IEncoding
che eredita
l'interfaccia generata.
class RiscV32IEncoding : public RiscV32IEncodingBase {
public:
};
Il costruttore deve quindi usare un puntatore all'istanza di stato,
un puntatore a riscv::RiscVState
. Deve essere usato il distruttore predefinito.
explicit RiscV32IEncoding(riscv::RiscVState *state);
~RiscV32IEncoding() override = default;
Prima di aggiungere tutti i metodi dell'interfaccia, aggiungiamo il metodo chiamato
RiscV32Decoder
per analizzare l'istruzione:
void ParseInstruction(uint32_t inst_word);
Ora aggiungiamo i metodi che hanno override banali pur eliminando dei parametri che non vengono utilizzati:
// Trivial overrides.
ResourceOperandInterface *GetSimpleResourceOperand(SlotEnum, int, OpcodeEnum,
SimpleResourceVector &,
int) override {
return nullptr;
}
ResourceOperandInterface *GetComplexResourceOperand(SlotEnum, int, OpcodeEnum,
ComplexResourceEnum ,
int, int) override {
return nullptr;
}
PredicateOperandInterface *GetPredicate(SlotEnum, int, OpcodeEnum,
PredOpEnum) override {
return nullptr;
}
int GetLatency(SlotEnum, int, OpcodeEnum, DestOpEnum, int) override { return 0; }
Infine, aggiungi gli altri override del metodo dell'interfaccia pubblica, ma con le implementazioni differite al file .cc.
OpcodeEnum GetOpcode(SlotEnum, int) override;
SourceOperandInterface *GetSource(SlotEnum , int, OpcodeEnum,
SourceOpEnum source_op, int) override;
DestinationOperandInterface *GetDestination(SlotEnum, int, OpcodeEnum,
DestOpEnum dest_op, int,
int latency) override;
Al fine di semplificare l'implementazione di ognuno dei metodi getter degli operandi
creeremo due array di callables (oggetti funzione) indicizzati dal
valore numerico rispettivamente dei membri SourceOpEnum
e DestOpEnum
.
In questo modo il corpo di questi elementi nei metodi è ridotto a chiamare il metodo
per il valore enum che viene passato e che restituisce come risultato
valore.
Per organizzare l'inizializzazione di questi due array, definiamo due che verranno chiamati dal costruttore nel seguente modo:
private:
void InitializeSourceOperandGetters();
void InitializeDestinationOperandGetters();
Membri dei dati
I membri dei dati richiesti sono i seguenti:
state_
per contenere il valoreriscv::RiscVState *
.inst_word_
di tipouint32_t
che contiene il valore del valore attuale parola di istruzione.opcode_
per conservare il codice operativo dell'istruzione corrente che viene aggiornato da il metodoParseInstruction
. Questo ha il tipoOpcodeEnum
.source_op_getters_
un array per archiviare i chiamabili utilizzati per ottenere l'origine oggetti operando. Il tipo di elementi di un array èabsl::AnyInvocable<SourceOperandInterface *>()>
.dest_op_getters_
un array per archiviare i chiamabili utilizzati per ottenere oggetti operando di destinazione. Il tipo di elementi di un array èabsl::AnyInvocable<DestinationOperandInterface *>()>
.xreg_alias
un array di nomi ABI del registro intero RiscV, ad esempio "zero" e "ra" anziché "x0" e "x1".
riscv::RiscVState *state_;
uint32_t inst_word_;
OpcodeEnum opcode_;
absl::AnyInvocable<SourceOperandInterface *()>
source_op_getters_[static_cast<int>(SourceOpEnum::kPastMaxValue)];
absl::AnyInvocable<DestinationOperandInterface *(int)>
dest_op_getters_[static_cast<int>(DestOpEnum::kPastMaxValue)];
const std::string xreg_alias_[32] = {
"zero", "ra", "sp", "gp", "tp", "t0", "t1", "t2", "s0", "s1", "a0",
"a1", "a2", "a3", "a4", "a5", "a6", "a7", "s2", "s3", "s4", "s5",
"s6", "s7", "s8", "s9", "s10", "s11", "t3", "t4", "t5", "t6"};
Se hai bisogno di aiuto (o vuoi controllare il tuo lavoro), la risposta completa è qui
File di origine (.cc).
Apri il file riscv32i_encoding.cc
. Tutti i file di inclusione necessari contengono
è già stato aggiunto e gli spazi dei nomi sono stati configurati. L'aggiunta di tutti i codici è
ha finito di seguire il commento // Exercise 2.
Funzioni helper
Inizieremo scrivendo un paio di funzioni helper che utilizziamo per creare
degli operandi dei registri di origine e di destinazione. Questi modelli verranno presi in considerazione
registry type e chiama l'oggetto RiscVState
per ottenere un handle
registro oggetto, quindi chiamare un metodo di fabbrica operando nell'oggetto registri.
Iniziamo con gli helper dell'operando di destinazione:
template <typename RegType>
inline DestinationOperandInterface *GetRegisterDestinationOp(
RiscVState *state, const std::string &name, int latency) {
auto *reg = state->GetRegister<RegType>(name).first;
return reg->CreateDestinationOperand(latency);
}
template <typename RegType>
inline DestinationOperandInterface *GetRegisterDestinationOp(
RiscVState *state, const std::string &name, int latency,
const std::string &op_name) {
auto *reg = state->GetRegister<RegType>(name).first;
return reg->CreateDestinationOperand(latency, op_name);
}
Come puoi vedere, ci sono due funzioni helper. Il secondo richiede un'ulteriore
parametro op_name
che consente all'operando di avere un nome o una stringa diversi
rappresentativa del registro di base.
In modo analogo per gli helper degli operandi di origine:
template <typename RegType>
inline SourceOperandInterface *GetRegisterSourceOp(RiscVState *state,
const std::string ®_name) {
auto *reg = state->GetRegister<RegType>(reg_name).first;
auto *op = reg->CreateSourceOperand();
return op;
}
template <typename RegType>
inline SourceOperandInterface *GetRegisterSourceOp(RiscVState *state,
const std::string ®_name,
const std::string &op_name) {
auto *reg = state->GetRegister<RegType>(reg_name).first;
auto *op = reg->CreateSourceOperand(op_name);
return op;
}
Funzioni costruttore e interfaccia
Il costruttore e le funzioni di interfaccia sono molto semplici. Il costruttore chiama semplicemente i due metodi di inizializzazione per inizializzare gli array callables per i getter dell'operando.
RiscV32IEncoding::RiscV32IEncoding(RiscVState *state) : state_(state) {
InitializeSourceOperandGetters();
InitializeDestinationOperandGetters();
}
ParseInstruction
memorizza la parola di istruzione e poi il codice operativo
ottiene richiamando il codice generato dal decoder binario.
// Parse the instruction word to determine the opcode.
void RiscV32IEncoding::ParseInstruction(uint32_t inst_word) {
inst_word_ = inst_word;
opcode_ = mpact::sim::codelab::DecodeRiscVInst32(inst_word_);
}
Infine, i getter dell'operando restituiscono il valore dalla funzione getter che chiama in base alla ricerca nell'array utilizzando il valore enum dell'operando destinazione/origine.
DestinationOperandInterface *RiscV32IEncoding::GetDestination(
SlotEnum, int, OpcodeEnum, DestOpEnum dest_op, int, int latency) {
return dest_op_getters_[static_cast<int>(dest_op)](latency);
}
SourceOperandInterface *RiscV32IEncoding::GetSource(SlotEnum, int, OpcodeEnum,
SourceOpEnum source_op, int) {
return source_op_getters_[static_cast<int>(source_op)]();
}
Metodi di inizializzazione degli array
Come avrai intuito, la maggior parte del lavoro consiste nell'inizializzare il comando getter
ma non preoccuparti, si può fare utilizzando una semplice sequenza ricorrente. Iniziamo
iniziano prima con InitializeDestinationOpGetters()
, poiché c'è solo
coppia di operandi di destinazione.
Ricorda il corso DestOpEnum
generato da riscv32i_enums.h
:
enum class DestOpEnum {
kNone = 0,
kCsr = 1,
kNextPc = 2,
kRd = 3,
kPastMaxValue = 4,
};
Per dest_op_getters_
dobbiamo inizializzare 4 voci, una per kNone
,
kCsr
, kNextPc
e kRd
. Per praticità, ogni voce viene inizializzata con
lambda, sebbene sia possibile usare anche qualsiasi altra forma di callable. La firma
di lambda è void(int latency)
.
Finora non abbiamo parlato molto dei diversi tipi di destinazioni
operandi definiti in MPACT-Sim. Per questo esercizio utilizzeremo solo due
tipi: generic::RegisterDestinationOperand
definiti in
register.h
,
e generic::DevNullOperand
definiti in
devnull_operand.h
.
I dettagli di questi operandi non sono molto importanti al momento, ad eccezione del fatto che
la prima è utilizzata per scrivere nei registri, mentre la seconda ignora tutte le scritture.
La prima voce per kNone
è banale: è sufficiente restituire un nullptr e, facoltativamente, il valore
registrare un errore.
void RiscV32IEncoding::InitializeDestinationOperandGetters() {
// Destination operand getters.
dest_op_getters_[static_cast<int>(DestOpEnum::kNone)] = [](int) {
return nullptr;
};
Poi c'è kCsr
. Qui andremo a imbrogliare un po'. Il "Hello World" programma
non si basa su alcun aggiornamento effettivo del CSR, ma c'è un codice boilerplate che
eseguire le istruzioni per la richiesta CSR. La soluzione è fare un esempio utilizzando un
registro regolare denominato "CSR" e incanalare tutte queste scritture.
dest_op_getters_[static_cast<int>(DestOpEnum::kCsr)] = [this](int latency) {
return GetRegisterDestinationOp<RV32Register>(state_, "CSR", latency);
};
Poi c'è kNextPc
, che fa riferimento al "pc" registro. È utilizzato come target
per tutte le istruzioni di ramo e salto. Il nome è definito in RiscVState
come
kPcName
.
dest_op_getters_[static_cast<int>(DestOpEnum::kNextPc)] = [this](int latency) {
return GetRegisterDestinationOp<RV32Register>(state_, RiscVState::kPcName, latency);
}
Infine c'è l'operando di destinazione kRd
. Nell'operando riscv32i.isa
rd
viene utilizzato solo per fare riferimento al registro intero codificato nel carattere "rd" campo
della parola di istruzione, per cui non vi sia ambiguità. Là
è solo una delle complicazioni. Il registro x0
(nome abi zero
) è cablato su 0,
quindi per il registro utilizziamo DevNullOperand
.
Quindi in questo getter estraiamo per prima cosa il valore nel campo rd
utilizzando
Metodo Extract
generato dal file .bin_fmt. Se il valore è 0,
restituisce un valore "DevNull" operando, altrimenti viene restituito l'operando di registro corretto.
avendo cura di usare l'alias del registro appropriato come nome dell'operando.
dest_op_getters_[static_cast<int>(DestOpEnum::kRd)] = [this](int latency) {
// First extract register number from rd field.
int num = inst32_format::ExtractRd(inst_word_);
// For register x0, return the DevNull operand.
if (num == 0) return new DevNullOperand<uint32_t>(state, {1});
// Return the proper register operand.
return GetRegisterDestinationOp<RV32Register>(
state_, absl::StrCat(RiscVState::kXRegPrefix, num), latency,
xreg_alias_[num]);
)
}
}
Passiamo ora al metodo InitializeSourceOperandGetters()
, dove il pattern è
più o meno lo stesso, ma i dettagli differiscono leggermente.
Innanzitutto, diamo un'occhiata al SourceOpEnum
generato
riscv32i.isa
nel primo tutorial:
enum class SourceOpEnum {
kNone = 0,
kBimm12 = 1,
kCsr = 2,
kImm12 = 3,
kJimm20 = 4,
kRs1 = 5,
kRs2 = 6,
kSimm12 = 7,
kUimm20 = 8,
kUimm5 = 9,
kPastMaxValue = 10,
};
Dall'esame dei membri, oltre a kNone
, rientrano in due gruppi. Uno.
sono operandi immediati: kBimm12
, kImm12
, kJimm20
, kSimm12
, kUimm20
,
e kUimm5
. Gli altri sono operandi di registri: kCsr
, kRs1
e kRs2
.
L'operando kNone
viene gestito come per gli operandi di destinazione. Restituisce un
nullptr.
void RiscV32IEncoding::InitializeSourceOperandGetters() {
// Source operand getters.
source_op_getters_[static_cast<int>(SourceOpEnum::kNone)] = [] () {
return nullptr;
};
Ora lavoriamo sugli operandi del registro. Gestiremo le kCsr
al modo in cui abbiamo gestito gli operandi di destinazione corrispondenti, basta richiamare
funzione helper utilizzando "CSR" come nome del registro.
// Register operands.
source_op_getters_[static_cast<int>(SourceOpEnum::kCsr)] = [this]() {
return GetRegisterSourceOp<RV32Register>(state_, "CSR");
};
Gli operando kRs1
e kRs2
vengono gestiti in modo equivalente a kRd
, ad eccezione di
anche se non volevamo aggiornare x0
(o zero
), vogliamo assicurarci
leggiamo sempre 0 dall'operando. A questo scopo utilizzeremo
generic::IntLiteralOperand<>
corso definito in
literal_operand.h
Questo operando viene utilizzato per archiviare un valore letterale (anziché un valore
valore immediato). In caso contrario, il pattern è lo stesso: estrai prima il
Il valore rs1/rs2 della parola di istruzione, se è zero, restituisce il valore letterale
operando con un parametro di modello pari a 0, altrimenti restituisce un registro regolare
operando di origine utilizzando la funzione helper e utilizzando l'alias abi come operando
nome.
source_op_getters_[static_cast<int>(SourceOpEnum::kRs1)] =
[this]() -> SourceOperandInterface * {
int num = inst32_format::ExtractRs1(inst_word_);
if (num == 0) return new IntLiteralOperand<0>({1}, xreg_alias_[0]);
return GetRegisterSourceOp<RV32Register>(
state_, absl::StrCat(RiscVState::kXregPrefix, num), xreg_alias_[num]);
};
source_op_getters_[static_cast<int>(SourceOpEnum::kRs2)] =
[this]() -> SourceOperandInterface * {
int num = inst32_format::ExtractRs2(inst_word_);
if (num == 0) return new IntLiteralOperand<0>({1}, xreg_alias_[0]);
return GetRegisterSourceOp<RV32Register>(
state_, absl::StrCat(RiscVState::kXregPrefix, num), xreg_alias_[num]);
};
Infine, gestiamo i diversi operandi immediati. I valori immediati sono
archiviati in istanze della classe generic::ImmediateOperand<>
definita
immediate_operand.h
L'unica differenza tra i diversi getter per gli operandi immediati
è la funzione estrattore utilizzata e se il tipo di archiviazione è firmato o
senza firma, in base al campo di bit.
// Immediates.
source_op_getters_[static_cast<int>(SourceOpEnum::kBimm12)] = [this]() {
return new ImmediateOperand<int32_t>(
inst32_format::ExtractBImm(inst_word_));
};
source_op_getters_[static_cast<int>(SourceOpEnum::kImm12)] = [this]() {
return new ImmediateOperand<int32_t>(
inst32_format::ExtractImm12(inst_word_));
};
source_op_getters_[static_cast<int>(SourceOpEnum::kUimm5)] = [this]() {
return new ImmediateOperand<uint32_t>(
inst32_format::ExtractUimm5(inst_word_));
};
source_op_getters_[static_cast<int>(SourceOpEnum::kJimm20)] = [this]() {
return new ImmediateOperand<int32_t>(
inst32_format::ExtractJImm(inst_word_));
};
source_op_getters_[static_cast<int>(SourceOpEnum::kSimm12)] = [this]() {
return new ImmediateOperand<int32_t>(
inst32_format::ExtractSImm(inst_word_));
};
source_op_getters_[static_cast<int>(SourceOpEnum::kUimm20)] = [this]() {
return new ImmediateOperand<uint32_t>(
inst32_format::ExtractUimm32(inst_word_));
};
}
Se hai bisogno di aiuto (o vuoi controllare il tuo lavoro), la risposta completa è qui
Il tutorial termina qui. Speriamo che ti sia stato utile.