Decoder Terintegrasi RiscV

Tujuan tutorial ini adalah:

  • Pelajari cara ISA dan dekoder biner yang dihasilkan cocok satu sama lain.
  • Tulis kode C++ yang diperlukan untuk membuat dekoder petunjuk lengkap untuk RiscV RV32I yang menggabungkan dekoder ISA dan biner.

Memahami dekoder petunjuk

Decoder instruksi bertanggung jawab untuk, dengan alamat instruksi, membaca kata petunjuk dari memori dan menampilkan instance Instruction yang diinisialisasi sepenuhnya yang merepresentasikan instruksi tersebut.

Decoder tingkat atas menerapkan generic::DecoderInterface yang ditampilkan di bawah ini:

// 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 diperlukan oleh kode yang dihasilkan.

Pertama, pertimbangkan class level teratas RiscV32IInstructionSet dalam file riscv32i_decoder.h, yang dibuat di akhir tutorial tentang decoder ISA. Untuk melihat konten yang baru, buka direktori solusi dari tutorial tersebut dan buat ulang semuanya.

$ cd riscv_isa_decoder/solution
$ bazel build :all
...<snip>...

Sekarang, ubah direktori Anda kembali ke root repositori, lalu mari kita lihat sumber yang dihasilkan. Untuk itu, ubah direktori ke bazel-out/k8-fastbuild/bin/riscv_isa_decoder (dengan asumsi Anda menggunakan host x86 - untuk host lain, k8-fastbuild akan menjadi string lain).

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

Anda akan melihat empat file sumber yang berisi kode C++ yang dihasilkan:

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

Buka file pertama riscv32i_decoder.h. Ada tiga class yang perlu kita lihat:

  • RiscV32IEncodingBase
  • RiscV32IInstructionSetFactory
  • RiscV32IInstructionSet

Perhatikan penamaan class. Semua class diberi nama berdasarkan versi Pascal-case dari nama yang diberikan dalam deklarasi "isa" dalam file tersebut: isa RiscV32I { ... }

Mari kita mulai dengan class RiscVIInstructionSet terlebih dahulu. Hal ini 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, sehingga ini adalah class mandiri, tetapi perhatikan dua hal. Pertama, konstruktor mengambil pointer ke instance class RiscV32IInstructionSetFactory. Ini adalah class yang digunakan dekoder yang dihasilkan untuk membuat instance class RiscV32Slot, yang digunakan untuk mendekode semua petunjuk yang ditentukan untuk slot RiscV32 seperti yang didefinisikan 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 decoder biner yang dihasilkan di lab kedua.

Class RiscV32IInstructionSetFactory adalah class abstrak yang harus kita dapatkan implementasinya sendiri untuk dekoder lengkap. Dalam sebagian besar kasus, class ini bersifat sepele: cukup berikan metode untuk memanggil konstruktor untuk setiap class slot yang ditentukan dalam file .isa. Dalam kasus kita, ini sangat sederhana karena hanya ada satu class tersebut: Riscv32Slot (Pascal-case dari nama riscv32 yang digabungkan dengan Slot). Metode ini tidak dibuat untuk Anda karena ada beberapa kasus penggunaan lanjutan yang mungkin memiliki utilitas dalam memperoleh subclass dari slot, dan memanggil konstruktornya.

Kita akan membahas class terakhir RiscV32IEncodingBase nanti dalam tutorial ini, karena ini merupakan subjek latihan lain.


Menentukan decoder petunjuk tingkat teratas

Menentukan class factory

Jika Anda mem-build ulang project untuk tutorial pertama, pastikan Anda kembali ke direktori riscv_full_decoder.

Buka file riscv32_decoder.h. Semua file include yang diperlukan telah ditambahkan dan namespace telah disiapkan.

Setelah komentar bertanda //Exercise 1 - step 1, tentukan class RiscV32IsaFactory yang mewarisi dari RiscV32IInstructionSetFactory.

class RiscV32IsaFactory : public RiscV32InstructionSetFactory {};

Selanjutnya, tentukan penggantian untuk CreateRiscv32Slot. Karena tidak menggunakan 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 memerlukan bantuan (atau ingin memeriksa pekerjaan Anda), jawaban lengkapnya ada di sini.

Menentukan class decoder

Deklarasi konstruktor, destruktor, dan metode

Selanjutnya, saatnya menentukan class decoder. Dalam file yang sama seperti di atas, buka deklarasi RiscV32Decoder. Luaskan deklarasi menjadi definisi class tempat RiscV32Decoder mewarisi dari generic::DecoderInterface.

class RiscV32Decoder : public generic::DecoderInterface {
  public:
};

Selanjutnya, sebelum menulis konstruktor, mari kita lihat sekilas kode yang dihasilkan dalam tutorial kedua tentang decoder biner. Selain semua fungsi Extract, ada fungsi DecodeRiscVInst32:

OpcodeEnum DecodeRiscVInst32(uint32_t inst_word);

Fungsi ini menggunakan kata petunjuk yang perlu didekode, dan menampilkan opcode yang cocok dengan petunjuk tersebut. Di sisi lain, class DecodeInterface yang diimplementasikan RiscV32Decoder hanya meneruskan alamat. Dengan demikian, class RiscV32Decoder harus dapat mengakses memori untuk membaca kata petunjuk yang akan diteruskan ke DecodeRiscVInst32(). Dalam project ini, cara mengakses memori adalah melalui antarmuka memori sederhana yang ditentukan di .../mpact/sim/util/memory yang dinamai dengan tepat util::MemoryInterface, yang terlihat di bawah:

  // 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 konstruktor class decoder lainnya. Class status yang sesuai adalah class riscv::RiscVState, yang berasal dari generic::ArchState, dengan fungsi tambahan untuk RiscV. Artinya, kita harus mendeklarasikan konstruktor agar dapat mengambil 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 dari generic::DecoderInterface.

generic::Instruction *DecodeInstruction(uint64_t address) override;

Jika Anda memerlukan bantuan (atau ingin memeriksa hasil pekerjaan Anda), jawaban lengkapnya ada di sini.


Definisi Anggota Data

Class RiscV32Decoder akan memerlukan anggota data pribadi untuk menyimpan parameter konstruktor dan pointer ke class factory.

 private:
  riscv::RiscVState *state_;
  util::MemoryInterface *memory_;

Class ini juga memerlukan pointer ke class encoding yang berasal dari RiscV32IEncodingBase, sebut saja RiscV32IEncoding (kita akan menerapkannya dalam latihan 2). Selain itu, pointer memerlukan instance RiscV32IInstructionSet, jadi tambahkan:

  RiscV32IsaFactory *riscv_isa_factory_;
  RiscV32IEncoding *riscv_encoding_;
  RiscV32IInstructionSet *riscv_isa_;

Terakhir, kita perlu menentukan anggota data untuk digunakan dengan antarmuka memori:

  generic::DataBuffer *inst_db_;

Jika Anda memerlukan bantuan (atau ingin memeriksa pekerjaan Anda), jawaban lengkapnya ada di sini.

Menentukan Metode Class Decoder

Selanjutnya, saatnya mengimplementasikan konstruktor, destruktor, dan metode DecodeInstruction. Buka file riscv32_decoder.cc. Metode kosong sudah ada dalam file serta deklarasi namespace dan beberapa deklarasi using.

Definisi Konstruktor

Konstruktor hanya perlu menginisialisasi anggota data. Pertama, lakukan inisialisasi 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. Ini dialokasikan menggunakan factory yang dapat diakses melalui anggota state_. Kita mengalokasikan buffer 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

Destruktor itu sederhana, cukup bebaskan objek yang kita alokasikan di konstruktor, tetapi dengan satu putaran. Instance buffering data dihitung referensi, jadi sebagai gantinya, bukan memanggil delete pada pointer tersebut, kita DecRef() 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. Kita akan mengasumsikan bahwa alamat sudah disejajarkan dengan benar dan tidak diperlukan pemeriksaan error tambahan.

Pertama, kata petunjuk harus diambil dari memori menggunakan antarmuka memori dan instance DataBuffer.

  memory_->Load(address, inst_db_, nullptr, nullptr);
  uint32_t iword = inst_db_->Get<uint32_t>(0);

Selanjutnya, kita memanggil instance RiscVIEncoding untuk mengurai kata petunjuk, yang harus dilakukan sebelum memanggil dekoder ISA itu sendiri. Ingat bahwa decoder ISA memanggil instance RiscVIEncoding secara langsung untuk mendapatkan opcode dan operand yang ditentukan oleh kata petunjuk. Kita belum mengimplementasikan class tersebut, tetapi mari kita gunakan void ParseInstruction(uint32_t) sebagai metode tersebut.

  riscv_encoding_->ParseInstruction(iword);

Terakhir, kita memanggil decoder ISA, yang meneruskan alamat dan class Encoding.

  auto *instruction = riscv_isa_->Decode(address, riscv_encoding_);
  return instruction;

Jika Anda memerlukan bantuan (atau ingin memeriksa pekerjaan Anda), jawaban lengkapnya ada di sini.


Class encoding

Class encoding mengimplementasikan antarmuka yang digunakan oleh class decoder untuk mendapatkan opcode petunjuk, operand sumber dan tujuannya, serta operand resource. Semua objek ini bergantung pada informasi dari decoder format biner, seperti opcode, nilai kolom tertentu dalam kata petunjuk, dll. Ini dipisahkan dari class decoder agar tetap mengenkodenya agnostik dan mengaktifkan dukungan untuk berbagai skema encoding yang berbeda di masa mendatang.

RiscV32IEncodingBase adalah class abstrak. Serangkaian metode yang harus kita implementasikan di 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;
};

Sekilas, ini terlihat agak rumit, terutama dengan jumlah parameter, tetapi untuk arsitektur sederhana seperti RiscV, kita sebenarnya mengabaikan sebagian besar parameter, karena nilainya akan tersirat.

Mari kita bahas setiap metodenya satu per satu.

OpcodeEnum GetOpcode(SlotEnum slot, int entry);

Metode GetOpcode menampilkan anggota OpcodeEnum untuk instruksi saat ini, yang mengidentifikasi opcode petunjuk. Class OpcodeEnum ditentukan dalam file decoder isa yang dihasilkan riscv32i_enums.h. Metode ini mengambil dua parameter, yang keduanya dapat diabaikan untuk tujuan kita. Yang pertama adalah jenis slot (class enum yang juga ditentukan di riscv32i_enums.h), yang, karena RiscV hanya memiliki satu slot, hanya memiliki satu kemungkinan nilai: SlotEnum::kRiscv32. Yang kedua adalah nomor instance slot (jika ada beberapa instance slot, yang mungkin terjadi di beberapa arsitektur VLIW).

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 menggunakannya, sehingga dalam implementasi, stub akan dihapus, yang 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 menampilkan pointer ke objek operand yang digunakan dalam fungsi semantik petunjuk untuk mengakses nilai operand predikat petunjuk, setiap operand sumber petunjuk, dan menulis nilai baru ke operand tujuan petunjuk. Karena RiscV tidak menggunakan predikat petunjuk, metode tersebut hanya perlu menampilkan nullptr.

Pola parameter serupa di seluruh fungsi ini. Pertama, sama seperti GetOpcode, slot dan entri diteruskan. Kemudian, opcode untuk instruksi yang mana operand harus dibuat. Hal ini hanya digunakan jika opcode yang berbeda perlu menampilkan objek operand yang berbeda untuk jenis operand yang sama, yang tidak berlaku untuk simulator RiscV ini.

Berikutnya adalah Predicate, Source, dan Destination, 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 melihat kembali file riscv32.isa, Anda akan melihat bahwa file ini sesuai dengan kumpulan nama operand sumber dan tujuan yang digunakan dalam deklarasi setiap petunjuk. Dengan menggunakan nama operand yang berbeda untuk operand yang mewakili jenis operand dan bitfield yang berbeda, penulisan class encoding menjadi 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 dapat mengabaikannya), dan untuk operand tujuan, latensi (dalam siklus) yang berlalu antara waktu perintah diterbitkan, dan hasil tujuan tersedia untuk petunjuk berikutnya. Dalam simulator kita, latensi ini akan menjadi 0, yang berarti bahwa petunjuk akan langsung menulis hasil ke register.

int GetLatency(SlotEnum slot, int entry, OpcodeEnum opcode,
                         DestOpEnum dest_op, int dest_no);

Fungsi akhir digunakan untuk mendapatkan latensi operand tujuan tertentu jika telah ditentukan sebagai * dalam file .isa. Hal ini jarang terjadi, dan tidak digunakan untuk simulator RiscV ini, sehingga implementasi fungsi ini hanya akan menampilkan 0.


Menentukan class encoding

File header (.h)

Metode

Buka file riscv32i_encoding.h. Semua file include yang diperlukan telah ditambahkan dan namespace telah disiapkan. Semua penambahan kode dilakukan setelah komentar // Exercise 2.

Mari kita mulai dengan menentukan class RiscV32IEncoding yang mewarisi dari antarmuka yang dihasilkan.

class RiscV32IEncoding : public RiscV32IEncodingBase {
 public:

};

Selanjutnya, konstruktor harus mengambil pointer ke instance status, dalam hal ini pointer ke riscv::RiscVState. Destruktor default harus digunakan.

explicit RiscV32IEncoding(riscv::RiscVState *state);
~RiscV32IEncoding() override = default;

Sebelum 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 trivial sambil menghapus 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 implementasi yang ditangguhkan 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 masing-masing anggota SourceOpEnum dan DestOpEnum. Dengan cara ini, isi dari kedua metode ini dikurangi menjadi memanggil objek fungsi untuk nilai enum yang diteruskan dan menampilkan nilai pengembaliannya.

Untuk mengatur inisialisasi kedua array ini, kita menentukan dua metode pribadi yang akan dipanggil dari konstruktor sebagai berikut:

 private:
  void InitializeSourceOperandGetters();
  void InitializeDestinationOperandGetters();

Anggota data

Anggota data yang diperlukan adalah sebagai berikut:

  • state_ untuk menyimpan nilai riscv::RiscVState *.
  • inst_word_ dari jenis uint32_t yang menyimpan nilai kata petunjuk saat ini.
  • opcode_ untuk menyimpan opcode dari petunjuk saat ini yang diperbarui oleh metode ParseInstruction. Ini memiliki jenis OpcodeEnum.
  • source_op_getters_ array untuk menyimpan callable yang digunakan untuk mendapatkan objek operand sumber. Jenis elemen array adalah absl::AnyInvocable<SourceOperandInterface *>()>.
  • dest_op_getters_ array untuk menyimpan callable yang digunakan untuk mendapatkan objek operand tujuan. Jenis elemen array adalah absl::AnyInvocable<DestinationOperandInterface *>()>.
  • xreg_alias array nama ABI register bilangan bulat RiscV, misalnya, "zero" 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 memerlukan bantuan (atau ingin memeriksa hasil pekerjaan Anda), jawaban lengkapnya ada di sini.

File sumber (.cc).

Buka file riscv32i_encoding.cc. Semua file include yang diperlukan telah ditambahkan dan namespace telah disiapkan. Semua penambahan kode dilakukan setelah komentar // Exercise 2.

Fungsi bantuan

Kita akan mulai dengan menulis beberapa fungsi bantuan yang kita gunakan untuk membuat Operand daftar sumber dan tujuan. Fungsi ini akan dijadikan template pada jenis register dan akan memanggil objek RiscVState untuk mendapatkan handle bagi objek register, lalu memanggil metode factory operand dalam 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 menggunakan parameter tambahan op_name yang memungkinkan operand memiliki nama, atau representasi string, yang berbeda dari register yang mendasarinya.

Demikian pula untuk helper operand sumber:

template <typename RegType>
inline SourceOperandInterface *GetRegisterSourceOp(RiscVState *state,
                                                   const std::string &reg_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 &reg_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

Konstruktor dan fungsi antarmuka sangat sederhana. Konstruktor hanya memanggil dua metode inisialisasi untuk menginisialisasi array callable untuk pengambil operand.

RiscV32IEncoding::RiscV32IEncoding(RiscVState *state) : state_(state) {
  InitializeSourceOperandGetters();
  InitializeDestinationOperandGetters();
}

ParseInstruction menyimpan kata petunjuk, lalu opcode yang diperolehnya dari memanggil 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 mungkin Anda duga, sebagian besar pekerjaannya adalah melakukan inisialisasi array pengambil, tetapi jangan khawatir, ini dilakukan menggunakan pola berulang yang mudah. Mari kita mulai dengan InitializeDestinationOpGetters() terlebih dahulu, karena hanya ada beberapa operand tujuan.

Ingat kembali 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 callable lainnya. Tanda tangan lambda adalah void(int latency).

Sampai sekarang, kita belum banyak membahas berbagai jenis operand tujuan yang ditentukan di MPACT-Sim. Untuk latihan ini, kita hanya akan menggunakan dua jenis: generic::RegisterDestinationOperand yang ditentukan dalam register.h, dan generic::DevNullOperand yang ditentukan dalam devnull_operand.h. Detail operand ini tidak terlalu penting saat ini, kecuali bahwa yang pertama digunakan untuk menulis ke register, dan yang kedua mengabaikan semua operasi tulis.

Entri pertama untuk kNone bersifat sepele - cukup tampilkan nullptr dan opsional catat error ke dalam log.

void RiscV32IEncoding::InitializeDestinationOperandGetters() {
  // Destination operand getters.
  dest_op_getters_[static_cast<int>(DestOpEnum::kNone)] = [](int) {
    return nullptr;
  };

Berikutnya adalah kCsr. Di sini kita akan menipu sedikit. Program "hello world" tidak bergantung pada update CSR yang sebenarnya, tetapi ada beberapa kode boilerplate yang mengeksekusi petunjuk CSR. Solusinya adalah dengan menirunya menggunakan register reguler bernama "CSR" dan menyalurkan semua penulisan tersebut ke sana.

  dest_op_getters_[static_cast<int>(DestOpEnum::kCsr)] = [this](int latency) {
    return GetRegisterDestinationOp<RV32Register>(state_, "CSR", latency);
  };

Berikutnya adalah kNextPc, yang mengacu pada register "pc". Ini digunakan sebagai target untuk semua petunjuk cabang dan lompat. Nama ditentukan dalam RiscVState sebagai kPcName.

  dest_op_getters_[static_cast<int>(DestOpEnum::kNextPc)] = [this](int latency) {
    return GetRegisterDestinationOp<RV32Register>(state_, RiscVState::kPcName, latency);
  }

Terakhir, ada operand tujuan kRd. Dalam riscv32i.isa operand rd hanya digunakan untuk merujuk ke register bilangan bulat yang dienkode di kolom "rd" dari kata petunjuk, sehingga tidak ada ambiguitas yang dirujuknya. Hanya ada satu detail. Register x0 (nama abi zero) di-hardcode ke 0, jadi untuk register tersebut, kita menggunakan DevNullOperand.

Jadi dalam pengambil ini, kita terlebih dahulu mengekstrak nilai di kolom rd menggunakan metode Extract yang dihasilkan dari file .bin_fmt. Jika nilainya 0, kita akan menampilkan operand "DevNull", jika tidak, kita akan menampilkan operand register yang benar, dengan berhati-hati 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(), dengan pola yang hampir sama, tetapi detailnya sedikit berbeda.

Pertama, mari kita lihat SourceOpEnum yang dihasilkan dari riscv32i.isa dalam 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,
  };

Selain kNone, anggota tersebut terbagi menjadi dua grup. Satu adalah operand langsung: kBimm12, kImm12, kJimm20, kSimm12, kUimm20, dan kUimm5. Yang lainnya adalah operand register: 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 kerjakan operand register. Kita akan menangani kCsr mirip dengan cara menangani operand tujuan yang sesuai - cukup panggil 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 memperbarui x0 (atau zero), kita ingin memastikan bahwa kita selalu membaca 0 dari operand tersebut. Untuk itu, kita akan menggunakan class generic::IntLiteralOperand<> yang ditentukan di literal_operand.h. Operand ini digunakan untuk menyimpan nilai literal (bukan nilai langsung yang disimulasikan). Jika tidak, polanya sama: pertama-tama ekstrak nilai rs1/rs2 dari kata perintah, jika nol, tampilkan operand literal dengan parameter template 0, jika tidak, tampilkan operand sumber register reguler menggunakan fungsi bantuan, menggunakan alias abi sebagai nama operand.

  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 berbagai operand langsung. Nilai langsung disimpan dalam instance class generic::ImmediateOperand<> yang ditentukan di immediate_operand.h. Satu-satunya perbedaan antara berbagai pengambil untuk operand langsung adalah fungsi Pengekstrak mana yang digunakan, dan apakah jenis penyimpanan ditandatangani atau tidak ditandatangani, sesuai dengan 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 memerlukan bantuan (atau ingin memeriksa hasil pekerjaan Anda), jawaban lengkapnya ada di sini.

Demikianlah tutorial ini. Semoga informasi ini bermanfaat.