Tujuan tutorial ini adalah:
- Pelajari cara decoder biner dan ISA yang dihasilkan cocok satu sama lain.
- Menulis kode C++ yang diperlukan untuk membuat decoder petunjuk lengkap untuk RiscV RV32I yang menggabungkan ISA dan decoder biner.
Memahami decoder instruksi
Decoder instruksi bertanggung jawab untuk, dengan alamat instruksi, pembacaan
kata instruksi dari memori dan mengembalikan instance dari
Instruction
yang mewakili petunjuk tersebut.
Decoder tingkat teratas mengimplementasikan generic::DecoderInterface
seperti yang ditunjukkan di bawah:
// 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;
};
Seperti yang dapat Anda lihat, hanya ada satu metode yang harus diimplementasikan: cpp
virtual Instruction *DecodeInstruction(uint64_t address);
Sekarang mari kita lihat apa yang disediakan dan apa yang dibutuhkan oleh kode yang dihasilkan.
Pertama, pertimbangkan class tingkat atas RiscV32IInstructionSet
dalam file
riscv32i_decoder.h
, yang dibuat di akhir tutorial tentang
Decoder ISA. Untuk melihat konten yang baru, arahkan ke direktori solusi
tutorial itu dan membangun ulang semuanya.
$ cd riscv_isa_decoder/solution
$ bazel build :all
...<snip>...
Sekarang ubah direktori Anda kembali ke
root repositori, lalu mari kita lihat
pada sumber yang dibuat. Untuk itu, ubah
direktori ke
bazel-out/k8-fastbuild/bin/riscv_isa_decoder
(dengan asumsi Anda menggunakan x86
{i>host<i} - untuk {i>host<i} lain, k8-fastbuild akan menjadi string lain).
$ cd ../..
$ cd bazel-out/k8-fastbuild/bin/riscv_isa_decoder
Anda akan melihat empat file sumber berisi kode C++ yang dihasilkan, yaitu:
riscv32i_decoder.h
riscv32i_decoder.cc
riscv32i_enums.h
riscv32i_enums.cc
Buka file pertama riscv32i_decoder.h
. Ada tiga class yang kita
perhatikan:
RiscV32IEncodingBase
RiscV32IInstructionSetFactory
RiscV32IInstructionSet
Perhatikan penamaan class. Semua class diberi nama berdasarkan
Versi Pascal-case dari nama yang diberikan dalam "isa" deklarasi dalam file tersebut:
isa RiscV32I { ... }
Mari kita mulai dengan class RiscVIInstructionSet
terlebih dahulu. Ditampilkan di bawah ini:
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_;
};
Tidak ada metode virtual di class ini, jadi ini adalah class yang berdiri sendiri, tetapi
memperhatikan dua hal. Pertama, konstruktor mengambil pointer ke sebuah instance
Class RiscV32IInstructionSetFactory
. Ini adalah class yang dihasilkan
digunakan decoder untuk membuat instance class RiscV32Slot
, yang digunakan
mendekode semua petunjuk yang ditentukan untuk slot RiscV32
seperti yang ditentukan dalam
File riscv32i.isa
. Kedua, metode Decode
mengambil parameter tambahan
jenis pointer ke RiscV32IEncodingBase
, ini adalah class yang akan menyediakan
antarmuka antara decoder isa yang dihasilkan dalam tutorial pertama dan biner
decoder yang dihasilkan di lab kedua.
Class RiscV32IInstructionSetFactory
adalah class abstrak tempat kita
harus memperoleh implementasi kita sendiri untuk decoder lengkap. Dalam kebanyakan kasus, proses ini
mudah: cukup sediakan metode untuk memanggil konstruktor untuk setiap class
yang ditentukan dalam file .isa
. Dalam kasus kami, sangat
sederhana karena ada
hanya satu class seperti itu: Riscv32Slot
(Pascal-case dari nama riscv32
yang disambungkan dengan Slot
). Metode ini tidak dibuat untuk Anda karena
beberapa kasus penggunaan lanjutan yang mungkin memiliki kegunaan dalam mendapatkan subclass
dari slot, dan memanggil konstruktornya.
Kita akan membahas kelas terakhir RiscV32IEncodingBase
nanti dalam
tutorial, karena ini adalah subjek latihan lain.
Menentukan decoder petunjuk tingkat teratas
Menentukan class factory
Jika Anda membuat ulang project untuk tutorial pertama, pastikan Anda mengubahnya kembali ke
direktori riscv_full_decoder
.
Buka file riscv32_decoder.h
. Semua file yang disertakan memiliki
ditambahkan dan namespace telah disiapkan.
Setelah komentar ditandai sebagai //Exercise 1 - step 1
, tentukan class
RiscV32IsaFactory
mewarisi dari RiscV32IInstructionSetFactory
.
class RiscV32IsaFactory : public RiscV32InstructionSetFactory {};
Berikutnya, tentukan penggantian untuk CreateRiscv32Slot
. Karena kita tidak
menggunakan
dari class turunan Riscv32Slot
, kita cukup mengalokasikan instance baru menggunakan
std::make_unique
.
std::unique_ptr<Riscv32Slot> CreateRiscv32Slot(ArchState *) override {
return std::make_unique<Riscv32Slot>(state);
}
Jika Anda membutuhkan bantuan (atau ingin memeriksa pekerjaan Anda), jawaban lengkapnya adalah di sini.
Menentukan class decoder
Konstruktor, destruktor, dan deklarasi metode
Selanjutnya, saatnya menentukan class decoder. Dalam file yang sama seperti di atas, buka
deklarasi RiscV32Decoder
. Luaskan deklarasi menjadi definisi class
dengan RiscV32Decoder
mewarisi dari generic::DecoderInterface
.
class RiscV32Decoder : public generic::DecoderInterface {
public:
};
Selanjutnya, sebelum menulis konstruktor, mari kita lihat sekilas kodenya
yang dihasilkan dalam tutorial kedua
tentang decoder biner. Selain semua
Extract
, terdapat fungsi DecodeRiscVInst32
:
OpcodeEnum DecodeRiscVInst32(uint32_t inst_word);
Fungsi ini mengambil kata petunjuk yang perlu didekode, dan menampilkan
opcode yang sesuai dengan instruksi itu. Di sisi lain,
Class DecodeInterface
yang diimplementasikan oleh RiscV32Decoder
hanya diteruskan dalam
alamat IPv6 Dengan demikian, class RiscV32Decoder
harus bisa mengakses memori untuk
baca kata petunjuk yang akan diteruskan ke DecodeRiscVInst32()
. Dalam project ini
cara untuk mengakses memori adalah melalui antarmuka
memori sederhana yang didefinisikan di
.../mpact/sim/util/memory
diberi nama yang tepat sebagai util::MemoryInterface
, seperti yang terlihat di bawah ini:
// 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;
Selain itu, kita harus dapat meneruskan instance class state
ke elemen
konstruktor class decoder lainnya. Class status yang sesuai adalah
Class riscv::RiscVState
, yang berasal dari generic::ArchState
, dengan tambahan
fungsionalitas untuk RiscV. Ini berarti kita harus mendeklarasikan konstruktor sehingga ia
dapat mengarahkan pointer ke state
dan memory
:
RiscV32Decoder(riscv::RiscVState *state, util::MemoryInterface *memory);
Hapus konstruktor default dan ganti destruktor:
RiscV32Decoder() = delete;
~RiscV32Decoder() override;
Selanjutnya, deklarasikan metode DecodeInstruction
yang perlu kita ganti
generic::DecoderInterface
.
generic::Instruction *DecodeInstruction(uint64_t address) override;
Jika Anda membutuhkan bantuan (atau ingin memeriksa pekerjaan Anda), jawaban lengkapnya adalah di sini.
Definisi Anggota Data
Class RiscV32Decoder
akan membutuhkan anggota data pribadi untuk menyimpan
parameter konstruktor dan pointer ke class factory.
private:
riscv::RiscVState *state_;
util::MemoryInterface *memory_;
Ia juga membutuhkan penunjuk ke
kelas pengkodean yang berasal dari
RiscV32IEncodingBase
, sebut saja RiscV32IEncoding
(kita akan mengimplementasikannya)
hal ini dalam latihan 2). Selain itu, skrip ini membutuhkan pointer ke instance
RiscV32IInstructionSet
, jadi tambahkan:
RiscV32IsaFactory *riscv_isa_factory_;
RiscV32IEncoding *riscv_encoding_;
RiscV32IInstructionSet *riscv_isa_;
Terakhir, kita harus menentukan anggota data untuk digunakan dengan antarmuka memori:
generic::DataBuffer *inst_db_;
Jika Anda membutuhkan bantuan (atau ingin memeriksa pekerjaan Anda), jawaban lengkapnya adalah di sini.
Menentukan Metode Class Decoder
Berikutnya, saatnya untuk mengimplementasikan
konstruktor, destruktor, dan
Metode DecodeInstruction
. Buka file riscv32_decoder.cc
. Kekosongan
sudah ada dalam file serta deklarasi namespace dan beberapa
dari using
deklarasi.
Definisi Konstruktor
Konstruktor hanya perlu melakukan inisialisasi anggota data. Pertama, inisialisasikan
state_
dan memory_
:
RiscV32Decoder::RiscV32Decoder(riscv::RiscVState *state,
util::MemoryInterface *memory)
: state_(state), memory_(memory) {
Selanjutnya, alokasikan instance dari setiap class terkait decoder, dengan meneruskan parameter yang sesuai.
// 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);
Terakhir, alokasikan instance DataBuffer
. Itu dialokasikan menggunakan factory
dapat diakses melalui anggota state_
. Kita mengalokasikan {i>buffer<i}
data yang berukuran untuk menyimpan
satu uint32_t
, karena itu adalah ukuran kata petunjuk.
inst_db_ = state_->db_factory()->Allocate<uint32_t>(1);
Definisi Destruktor
Destruktornya sederhana, cukup bebaskan objek yang kita alokasikan dalam konstruktor,
tetapi dengan satu putaran. Instance buffer data dihitung dengan referensi, jadi sebagai gantinya
memanggil delete
pada pointer tersebut, kita melakukan DecRef()
pada objek:
RiscV32Decoder::~RiscV32Decoder() {
inst_db_->DecRef();
delete riscv_isa_;
delete riscv_isa_factory_;
delete riscv_encoding_;
}
Definisi metode
Dalam kasus kita, implementasi metode ini cukup sederhana. Kami akan mengasumsikan bahwa alamat sudah disejajarkan dengan benar dan tidak ada pemeriksaan {i>error<i} tambahan yang tidak diperlukan.
Pertama, kata instruksi harus diambil
dari memori menggunakan memori
dan instance DataBuffer
.
memory_->Load(address, inst_db_, nullptr, nullptr);
uint32_t iword = inst_db_->Get<uint32_t>(0);
Selanjutnya, kita panggil instance RiscVIEncoding
untuk mengurai kata petunjuk,
yang harus dilakukan sebelum memanggil decoder ISA itu sendiri. Ingatlah bahwa ISA
decoder memanggil instance RiscVIEncoding
secara langsung untuk mendapatkan opcode
dan operand yang ditetapkan oleh kata instruksi. Kita belum menerapkan
sekarang, tetapi mari kita gunakan void ParseInstruction(uint32_t)
sebagai metode tersebut.
riscv_encoding_->ParseInstruction(iword);
Terakhir, kita memanggil decoder ISA, dengan meneruskan alamat dan class Encoding.
auto *instruction = riscv_isa_->Decode(address, riscv_encoding_);
return instruction;
Jika Anda membutuhkan bantuan (atau ingin memeriksa pekerjaan Anda), jawaban lengkapnya adalah di sini.
Class encoding
Class encoding mengimplementasikan antarmuka yang digunakan oleh class decoder untuk memperoleh opcode instruksi, operand sumber dan tujuannya, serta operand resource. Semua objek ini tergantung pada informasi dari sistem biner format decoder, seperti opcode, nilai kolom tertentu dalam kata instruksi dll. Ini dipisahkan dari kelas decoder untuk membuatnya encoding agnostik dan mengaktifkan dukungan untuk beberapa skema encoding yang berbeda di masa mendatang.
RiscV32IEncodingBase
adalah class abstrak. Serangkaian metode yang harus
terapkan dalam class turunan ditunjukkan di bawah ini.
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;
};
Sepintas terlihat agak rumit, terutama dengan jumlah parameter, tetapi untuk arsitektur sederhana seperti RiscV, kita mengabaikan sebagian besar parameter, karena nilainya akan tersirat.
Mari kita bahas tiap metode secara bergantian.
OpcodeEnum GetOpcode(SlotEnum slot, int entry);
Metode GetOpcode
menampilkan anggota OpcodeEnum
untuk aktivitas saat ini
instruksi, yang mengidentifikasi opcode instruksi. Class OpcodeEnum
yang ditentukan dalam file decoder yang dihasilkan riscv32i_enums.h
. Metode ini mengambil
dua parameter, yang keduanya dapat
diabaikan untuk tujuan kita. Yang pertama dari
ini adalah jenis slot (class enum yang juga ditentukan dalam riscv32i_enums.h
),
yang, karena RiscV hanya memiliki satu slot, hanya memiliki satu kemungkinan nilai:
SlotEnum::kRiscv32
. Yang kedua adalah nomor {i>
instance<i} slot (dalam hal
terdapat beberapa penggunaan slot, yang
dapat terjadi di beberapa VLIW
lainnya).
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);
Dua metode berikutnya digunakan untuk memodelkan resource hardware dalam prosesor
guna meningkatkan akurasi siklus. Untuk latihan tutorial, kita tidak akan menggunakan
ini, sehingga dalam implementasinya, parameter tersebut akan dinonaktifkan sehingga menampilkan 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);
Ketiga metode ini mengembalikan pointer ke objek operand yang digunakan dalam
fungsi semantik petunjuk untuk mengakses nilai instruksi apa pun
predikat operand, masing-masing operand sumber instruksi, dan menulis
nilai ke operand tujuan petunjuk. Karena RiscV tidak menggunakan
predikat instruksi, metode tersebut hanya perlu menampilkan nullptr
.
Pola parameter serupa di seluruh fungsi ini. Pertama, seperti
GetOpcode
slot dan entri diteruskan. Kemudian, opcode untuk
instruksi yang membuat operand harus dibuat. Metode ini hanya digunakan jika
opcode yang berbeda perlu menampilkan objek operand yang berbeda untuk operand yang sama
, yang tidak berlaku untuk simulator RiscV ini.
Berikutnya adalah Predikat, Sumber, dan Tujuan, entri enumerasi operand yang
mengidentifikasi operand yang harus dibuat. Ini berasal dari tiga
OpEnums di riscv32i_enums.h
seperti yang terlihat di bawah ini:
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,
};
Jika Anda melihat kembali
riscv32.isa
, Anda akan mencatat bahwa ini sesuai dengan kumpulan sumber dan tujuan
nama operand yang digunakan dalam deklarasi setiap instruksi. Dengan menggunakan berbagai
nama operand untuk operand yang merepresentasikan bitfield dan operand yang berbeda
, itu membuat penulisan class encoding lebih mudah karena anggota enum secara unik
menentukan jenis operand yang tepat untuk ditampilkan, dan tidak perlu
mempertimbangkan nilai parameter slot, entri, atau opcode.
Terakhir, untuk operand sumber dan tujuan, posisi ordinal operand diteruskan (sekali lagi, kita bisa mengabaikannya), dan untuk tujuan operand, latensi (dalam siklus) yang berlalu di antara waktu instruksi dikeluarkan, dan hasil tujuan tersedia untuk petunjuk berikutnya. Dalam simulator kita, latensi ini akan menjadi 0, yang berarti bahwa instruksi menulis hasilnya segera dikirim ke register.
int GetLatency(SlotEnum slot, int entry, OpcodeEnum opcode,
DestOpEnum dest_op, int dest_no);
Fungsi akhir digunakan untuk mendapatkan latensi tujuan tertentu
operand jika telah ditetapkan sebagai *
dalam file .isa
. Ini jarang terjadi,
dan tidak digunakan untuk simulator RiscV ini, jadi implementasi fungsi ini
hanya akan
menghasilkan 0.
Menentukan class encoding
File header (.h)
Metode
Buka file riscv32i_encoding.h
. Semua file yang disertakan memiliki
ditambahkan dan namespace telah disiapkan. Semua penambahan kode berupa
selesai mengikuti komentar // Exercise 2.
Mari kita mulai dengan menentukan class RiscV32IEncoding
yang diwarisi dari
dan antarmuka yang dihasilkan.
class RiscV32IEncoding : public RiscV32IEncodingBase {
public:
};
Selanjutnya, konstruktor harus membawa pointer ke instance status, dalam hal ini
pointer ke riscv::RiscVState
. Destruktor default harus digunakan.
explicit RiscV32IEncoding(riscv::RiscVState *state);
~RiscV32IEncoding() override = default;
Sebelum kita menambahkan semua metode antarmuka, mari kita tambahkan metode yang dipanggil oleh
RiscV32Decoder
untuk mengurai petunjuk:
void ParseInstruction(uint32_t inst_word);
Selanjutnya, mari tambahkan metode yang memiliki penggantian sepele saat melepaskan nama parameter yang tidak digunakan:
// 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; }
Terakhir, tambahkan penggantian metode yang tersisa dari antarmuka publik tetapi dengan implementasinya dialihkan ke 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;
Untuk menyederhanakan implementasi setiap metode pengambil operand
kita akan membuat dua array callable (objek fungsi) yang diindeks oleh
nilai numerik dari anggota SourceOpEnum
dan DestOpEnum
masing-masing.
Dengan cara ini isi ke metode direduksi menjadi memanggil
untuk nilai enum yang diteruskan dan menampilkan hasilnya
dengan sejumlah nilai.
Untuk mengatur inisialisasi kedua array ini, kita menentukan dua array metode yang akan dipanggil dari konstruktor sebagai berikut:
private:
void InitializeSourceOperandGetters();
void InitializeDestinationOperandGetters();
Anggota data
Anggota data yang diperlukan adalah sebagai berikut:
state_
untuk menyimpan nilairiscv::RiscVState *
.inst_word_
dari jenisuint32_t
yang menyimpan nilai saat ini kata instruksi.opcode_
untuk menyimpan opcode dari instruksi saat ini yang diupdate oleh metodeParseInstruction
. Class ini memiliki jenisOpcodeEnum
.source_op_getters_
array untuk menyimpan callable yang digunakan untuk mendapatkan sumber objek operand. Jenis elemen array adalahabsl::AnyInvocable<SourceOperandInterface *>()>
.dest_op_getters_
array untuk menyimpan callable yang digunakan untuk mendapatkan objek operand tujuan. Jenis elemen array adalahabsl::AnyInvocable<DestinationOperandInterface *>()>
.xreg_alias
array nama ABI pendaftaran bilangan bulat RiscV, mis., "nol" dan "ra" bukan "x0" dan "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"};
Jika Anda membutuhkan bantuan (atau ingin memeriksa pekerjaan Anda), jawaban lengkapnya adalah di sini.
File sumber (.cc).
Buka file riscv32i_encoding.cc
. Semua file yang disertakan memiliki
ditambahkan dan namespace telah disiapkan. Semua penambahan kode berupa
selesai mengikuti komentar // Exercise 2.
Fungsi bantuan
Kita akan mulai dengan menulis beberapa fungsi bantuan yang kita gunakan untuk membuat
operand daftar sumber dan tujuan. Template di
jenis register dan akan memanggil objek RiscVState
guna mendapatkan handle ke
daftarkan, dan kemudian panggil metode factory operand di objek register.
Mari kita mulai dengan helper operand tujuan:
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);
}
Seperti yang Anda lihat, ada dua fungsi bantuan. Yang kedua membutuhkan
parameter op_name
yang memungkinkan operand memiliki nama atau string yang berbeda
representasinya, daripada register yang mendasarinya.
Demikian pula untuk helper operand sumber:
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;
}
Fungsi konstruktor dan antarmuka
Fungsi konstruktor dan antarmukanya sangat sederhana. Konstruktor cukup memanggil dua metode inisialisasi untuk menginisialisasi array callables untuk pengambil operand.
RiscV32IEncoding::RiscV32IEncoding(RiscVState *state) : state_(state) {
InitializeSourceOperandGetters();
InitializeDestinationOperandGetters();
}
ParseInstruction
menyimpan kata petunjuk, lalu opcode yang
diperoleh dari panggilan ke kode yang dihasilkan decoder biner.
// 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_);
}
Terakhir, pengambil operand menampilkan nilai dari fungsi pengambil yang dipanggilnya berdasarkan pencarian array menggunakan nilai enum operand tujuan/sumber.
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)]();
}
Metode inisialisasi array
Seperti yang Anda duga, sebagian besar pekerjaan adalah menginisialisasi pengambil
{i>array<i}, tapi jangan khawatir, ini dilakukan
menggunakan pola berulang yang mudah. Mari kita
diawali dengan InitializeDestinationOpGetters()
terlebih dahulu, karena hanya ada
beberapa operand tujuan.
Ingat class DestOpEnum
yang dihasilkan dari riscv32i_enums.h
:
enum class DestOpEnum {
kNone = 0,
kCsr = 1,
kNextPc = 2,
kRd = 3,
kPastMaxValue = 4,
};
Untuk dest_op_getters_
, kita perlu melakukan inisialisasi 4 entri, masing-masing untuk kNone
,
kCsr
, kNextPc
, dan kRd
. Untuk memudahkan, setiap entri diinisialisasi dengan
lambda, meskipun Anda juga dapat menggunakan bentuk lain dari {i>callable<i}. Tanda tangan
lambda adalah void(int latency)
.
Hingga saat ini kita belum banyak membicarakan
berbagai jenis tujuan
operand yang ditentukan dalam MPACT-Sim. Untuk latihan ini kita hanya akan
menggunakan dua
jenis: generic::RegisterDestinationOperand
ditentukan di
register.h
,
dan generic::DevNullOperand
ditentukan di
devnull_operand.h
Detail operand ini tidak terlalu penting untuk saat ini, kecuali bahwa
yang pertama digunakan untuk menulis ke register, dan yang kedua mengabaikan semua operasi tulis.
Entri pertama untuk kNone
mudah - cukup tampilkan nullptr dan jika perlu
mencatat error.
void RiscV32IEncoding::InitializeDestinationOperandGetters() {
// Destination operand getters.
dest_op_getters_[static_cast<int>(DestOpEnum::kNone)] = [](int) {
return nullptr;
};
Berikutnya adalah kCsr
. Di sini kita akan sedikit menipu. "Halo dunia" program
tidak bergantung pada pembaruan CSR aktual, tetapi ada beberapa kode boilerplate yang
mengeksekusi instruksi CSR. Solusinya adalah dengan hanya menirunya dengan menggunakan
daftar reguler bernama "CSR" dan menyalurkan semua operasi tulis tersebut.
dest_op_getters_[static_cast<int>(DestOpEnum::kCsr)] = [this](int latency) {
return GetRegisterDestinationOp<RV32Register>(state_, "CSR", latency);
};
Berikutnya adalah kNextPc
, yang mengacu pada "pc" mendaftar. Model ini digunakan sebagai target
untuk semua instruksi cabang dan jump. Nama ditentukan di RiscVState
sebagai
kPcName
.
dest_op_getters_[static_cast<int>(DestOpEnum::kNextPc)] = [this](int latency) {
return GetRegisterDestinationOp<RV32Register>(state_, RiscVState::kPcName, latency);
}
Terakhir adalah operand tujuan kRd
. Di riscv32i.isa
operand
rd
hanya digunakan untuk merujuk ke register bilangan bulat yang dienkode dalam "rd" kolom
kata instruksi, jadi tidak ada ambiguitas yang merujuknya. Ada
hanya merupakan satu komplikasi. Daftarkan x0
(nama abi zero
) terhubung ke 0,
jadi untuk register itu kita gunakan DevNullOperand
.
Jadi, dalam pengambil ini, pertama-tama kita mengekstrak nilai di kolom rd
menggunakan
Metode Extract
yang dibuat dari file .bin_fmt. Jika nilainya 0, kita
mengembalikan "DevNull" operand, jika tidak, kita menampilkan operand register yang benar,
berhati-hati untuk menggunakan alias register yang sesuai sebagai nama operand.
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]);
)
}
}
Sekarang ke metode InitializeSourceOperandGetters()
, tempat polanya
hampir sama, tetapi detailnya sedikit berbeda.
Pertama, mari kita lihat SourceOpEnum
yang dihasilkan dari
riscv32i.isa
di tutorial pertama:
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,
};
Saat memeriksa anggota, selain kNone
, mereka termasuk dalam dua kelompok. paket Premium AI
adalah operand langsung: kBimm12
, kImm12
, kJimm20
, kSimm12
, kUimm20
,
dan kUimm5
. Satunya lagi adalah operand pendaftaran: kCsr
, kRs1
, dan kRs2
.
Operand kNone
ditangani seperti untuk operand tujuan - menampilkan
nullptr.
void RiscV32IEncoding::InitializeSourceOperandGetters() {
// Source operand getters.
source_op_getters_[static_cast<int>(SourceOpEnum::kNone)] = [] () {
return nullptr;
};
Selanjutnya, mari kita bekerja pada operand register. Kami akan menangani kCsr
yang serupa
bagaimana kita menangani operand tujuan yang sesuai - cukup panggil metode
fungsi bantuan menggunakan "CSR" sebagai nama register.
// Register operands.
source_op_getters_[static_cast<int>(SourceOpEnum::kCsr)] = [this]() {
return GetRegisterSourceOp<RV32Register>(state_, "CSR");
};
Operand kRs1
dan kRs2
ditangani secara setara dengan kRd
, kecuali bahwa
meskipun kita tidak ingin mengupdate x0
(atau zero
), kita ingin memastikan bahwa
kita selalu membaca 0 dari operand tersebut. Untuk melakukannya, kita akan menggunakan
Class generic::IntLiteralOperand<>
ditentukan di
literal_operand.h
.
Operand ini digunakan untuk menyimpan nilai literal (berlawanan dengan simulasi)
nilai langsung). Jika tidak, polanya sama: pertama-tama ekstrak
nilai rs1/rs2 dari kata instruksi, jika nol, maka akan menampilkan
operand dengan parameter template 0, jika tidak, tampilkan register reguler
operand sumber menggunakan fungsi bantuan, menggunakan alias abi sebagai operand
nama.
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]);
};
Terakhir, kita menangani operand langsung yang berbeda. Nilai langsung adalah
disimpan dalam instance class generic::ImmediateOperand<>
yang ditentukan di
immediate_operand.h
.
Satu-satunya perbedaan antara pengambil yang berbeda untuk operand langsung
adalah fungsi Ekstraktor yang digunakan, dan apakah
jenis penyimpanan ditandatangani atau
tidak ditandatangani, menurut bitfield.
// 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_));
};
}
Jika Anda membutuhkan bantuan (atau ingin memeriksa pekerjaan Anda), jawaban lengkapnya adalah di sini.
Demikianlah tutorial ini. Kami berharap ini bermanfaat.