Decoder Terintegrasi RiscV

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 nilai riscv::RiscVState *.
  • inst_word_ dari jenis uint32_t yang menyimpan nilai saat ini kata instruksi.
  • opcode_ untuk menyimpan opcode dari instruksi saat ini yang diupdate oleh metode ParseInstruction. Class ini memiliki jenis OpcodeEnum.
  • source_op_getters_ array untuk menyimpan callable yang digunakan untuk mendapatkan sumber objek operand. 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 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 &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

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.