Bộ giải mã tích hợp RiscV

Mục tiêu của hướng dẫn này là:

  • Tìm hiểu cách ISA và bộ giải mã nhị phân đã tạo kết hợp với nhau.
  • Viết mã C++ cần thiết để tạo bộ giải mã lệnh đầy đủ cho RiscV RV32I kết hợp ISA và bộ giải mã nhị phân.

Tìm hiểu bộ giải mã lệnh

Bộ giải mã lệnh chịu trách nhiệm đọc một địa chỉ lệnh được cung cấp từ lệnh khỏi bộ nhớ và trả về một thực thể đã khởi tạo đầy đủ của Instruction đại diện cho lệnh đó.

Bộ giải mã cấp cao nhất triển khai generic::DecoderInterface hiển thị bên dưới:

// 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;
};

Như bạn có thể thấy, chỉ có một phương thức phải được triển khai: cpp virtual Instruction *DecodeInstruction(uint64_t address);

Bây giờ, hãy xem những gì được cung cấp và những gì mà mã được tạo cần có.

Trước tiên, hãy xem xét lớp cấp cao nhất RiscV32IInstructionSet trong tệp riscv32i_decoder.h được tạo ở cuối hướng dẫn về Bộ giải mã ISA. Để xem lại nội dung, hãy chuyển đến thư mục giải pháp của hướng dẫn đó và xây dựng lại tất cả.

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

Bây giờ, hãy thay đổi thư mục của bạn về thư mục gốc của kho lưu trữ, sau đó hãy cùng xem tại các nguồn đã được tạo. Để làm được việc đó, hãy thay đổi thư mục thành bazel-out/k8-fastbuild/bin/riscv_isa_decoder (giả sử bạn đang dùng x86 host - đối với các máy chủ lưu trữ khác, k8-fastbuild sẽ là một chuỗi khác).

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

Bạn sẽ thấy bốn tệp nguồn chứa mã C++ đã tạo được liệt kê:

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

Mở tệp đầu tiên riscv32i_decoder.h. Có 3 lớp cần xem xét:

  • RiscV32IEncodingBase
  • RiscV32IInstructionSetFactory
  • RiscV32IInstructionSet

Vui lòng lưu ý cách đặt tên cho lớp. Tất cả các lớp đều được đặt tên dựa trên Phiên bản viết hoa Pascal của tên được đặt trong "isa" trong tệp đó: isa RiscV32I { ... }

Trước tiên, hãy bắt đầu với lớp RiscVIInstructionSet. Thông tin đó được hiển thị bên dưới:

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_;
};

Không có phương thức ảo trong lớp này, vì vậy, đây là một lớp độc lập, nhưng chú ý hai điều. Đầu tiên, hàm khởi tạo lấy một con trỏ đến một thực thể của hàm Lớp RiscV32IInstructionSetFactory. Đây là một lớp mà lớp đã tạo bộ giải mã dùng để tạo một thực thể của lớp RiscV32Slot, được dùng để giải mã tất cả lệnh được xác định cho slot RiscV32 như được xác định trong riscv32i.isa. Thứ hai, phương thức Decode lấy thêm một tham số con trỏ kiểu đến RiscV32IEncodingBase, đây là lớp sẽ cung cấp giao diện giữa bộ giải mã isa được tạo trong hướng dẫn đầu tiên và tệp nhị phân được tạo trong phòng thí nghiệm thứ hai.

Lớp RiscV32IInstructionSetFactory là một lớp trừu tượng mà từ đó chúng ta phải tìm ra cách triển khai của riêng chúng ta cho bộ giải mã đầy đủ. Trong hầu hết các trường hợp, lớp này không đáng kể: chỉ cần cung cấp một phương thức để gọi hàm khởi tạo cho mỗi lớp đã xác định trong tệp .isa. Trong trường hợp này, rất đơn giản chỉ là một lớp như vậy: Riscv32Slot (Pascal-case của tên riscv32 nối với Slot). Phương thức này không được tạo cho bạn vì một số trường hợp sử dụng nâng cao có thể giúp ích cho việc lấy một lớp con từ vị trí rồi gọi hàm khởi tạo của nó.

Chúng ta sẽ tìm hiểu về lớp cuối cùng RiscV32IEncodingBase ở phần sau trong nội dung này vì đó là chủ đề của một bài tập khác.


Xác định bộ giải mã lệnh cấp cao nhất

Xác định lớp factory

Nếu bạn đã tạo lại dự án cho hướng dẫn đầu tiên, hãy đảm bảo bạn quay lại thư mục riscv_full_decoder.

Mở tệp riscv32_decoder.h. Tất cả những thông tin cần thiết, bao gồm cả tệp có đã được thêm và không gian tên đã được thiết lập.

Sau khi nhận xét được đánh dấu //Exercise 1 - step 1, hãy xác định lớp RiscV32IsaFactory kế thừa từ RiscV32IInstructionSetFactory.

class RiscV32IsaFactory : public RiscV32InstructionSetFactory {};

Tiếp theo, hãy xác định chế độ ghi đè cho CreateRiscv32Slot. Vì chúng tôi không sử dụng các lớp dẫn xuất của Riscv32Slot, chúng ta chỉ cần phân bổ một thực thể mới sử dụng std::make_unique

std::unique_ptr<Riscv32Slot> CreateRiscv32Slot(ArchState *) override {
  return std::make_unique<Riscv32Slot>(state);
}

Nếu bạn cần trợ giúp (hoặc muốn kiểm tra bài tập của mình), câu trả lời đầy đủ là tại đây.

Xác định lớp bộ giải mã

Khai báo hàm khởi tạo, hàm khởi tạo và phương thức

Tiếp theo là thời điểm xác định lớp bộ giải mã. Trong cùng tệp như trên, hãy chuyển đến của RiscV32Decoder. Mở rộng phần khai báo thành một định nghĩa lớp trong đó RiscV32Decoder kế thừa từ generic::DecoderInterface.

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

Tiếp theo, trước khi viết hàm khởi tạo, hãy xem nhanh mã được tạo trong hướng dẫn thứ hai của chúng tôi về bộ giải mã nhị phân. Ngoài tất cả Hàm Extract, có hàm DecodeRiscVInst32:

OpcodeEnum DecodeRiscVInst32(uint32_t inst_word);

Hàm này lấy từ lệnh cần được giải mã rồi trả về mã vận hành khớp với lệnh đó. Mặt khác, Lớp DecodeInterfaceRiscV32Decoder chỉ triển khai thẻ truyền trong một của bạn. Do đó, lớp RiscV32Decoder phải có khả năng truy cập bộ nhớ để đọc từ hướng dẫn để chuyển đến DecodeRiscVInst32(). Trong dự án này bạn có thể truy cập bộ nhớ thông qua một giao diện bộ nhớ đơn giản được xác định trong .../mpact/sim/util/memory được đặt tên phù hợp là util::MemoryInterface, được xem bên dưới:

  // 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;

Ngoài ra, chúng ta cần có khả năng truyền một thực thể lớp state cho hàm khởi tạo của các lớp bộ giải mã khác. Lớp trạng thái thích hợp là Lớp riscv::RiscVState bắt nguồn từ generic::ArchState, đã được thêm vào cho RiscV. Tức là chúng ta phải khai báo hàm khởi tạo để nó có thể đưa con trỏ đến statememory:

RiscV32Decoder(riscv::RiscVState *state, util::MemoryInterface *memory);

Xoá hàm khởi tạo mặc định và ghi đè hàm khởi tạo:

RiscV32Decoder() = delete;
~RiscV32Decoder() override;

Tiếp theo, hãy khai báo phương thức DecodeInstruction mà chúng ta cần ghi đè generic::DecoderInterface

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

Nếu bạn cần trợ giúp (hoặc muốn kiểm tra bài tập của mình), câu trả lời đầy đủ là tại đây.


Định nghĩa về thành phần dữ liệu

Lớp RiscV32Decoder sẽ cần các thành phần dữ liệu riêng tư để lưu trữ các tham số hàm khởi tạo và một con trỏ đến lớp nhà máy.

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

Lớp mã hoá cũng cần một con trỏ tới lớp mã hoá bắt nguồn từ RiscV32IEncodingBase, hãy gọi hàm đó là RiscV32IEncoding (chúng ta sẽ triển khai trong bài tập 2). Ngoài ra, thành phần này cần con trỏ đến một thực thể của RiscV32IInstructionSet, vì vậy, hãy thêm:

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

Cuối cùng, chúng ta cần xác định một thành phần dữ liệu để sử dụng với giao diện bộ nhớ:

  generic::DataBuffer *inst_db_;

Nếu bạn cần trợ giúp (hoặc muốn kiểm tra bài tập của mình), câu trả lời đầy đủ là tại đây.

Xác định phương thức của lớp giải mã

Tiếp theo, đã đến lúc triển khai hàm khởi tạo, hàm khởi tạo và hàm DecodeInstruction. Mở tệp riscv32_decoder.cc. Khoảng trống đã có trong tệp cũng như các khai báo không gian tên và một vài trong số using nội dung khai báo.

Định nghĩa hàm khởi tạo

Hàm khởi tạo chỉ cần khởi tạo các thành phần dữ liệu. Trước tiên, hãy khởi chạy state_memory_:

RiscV32Decoder::RiscV32Decoder(riscv::RiscVState *state,
                               util::MemoryInterface *memory)
    : state_(state), memory_(memory) {

Tiếp theo, phân bổ các bản sao của từng lớp liên quan đến bộ giải mã, truyền vào các tham số phù hợp.

  // 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);

Cuối cùng, hãy phân bổ thực thể DataBuffer. Hàm này được phân bổ bằng một factory có thể truy cập thông qua thành phần state_. Chúng tôi phân bổ một vùng đệm dữ liệu có kích thước để lưu trữ một uint32_t, vì đó là kích thước của từ hướng dẫn.

  inst_db_ = state_->db_factory()->Allocate<uint32_t>(1);

Định nghĩa về thành phần phá huỷ

Hàm huỷ rất đơn giản, chỉ cần giải phóng các đối tượng chúng ta đã phân bổ trong hàm khởi tạo, nhưng có một thay đổi nhỏ. Thực thể vùng đệm dữ liệu được tính tham chiếu, vì vậy, thay vào đó khi gọi delete trên con trỏ đó, chúng ta DecRef() đối tượng:

RiscV32Decoder::~RiscV32Decoder() {
  inst_db_->DecRef();
  delete riscv_isa_;
  delete riscv_isa_factory_;
  delete riscv_encoding_;
}

Định nghĩa phương thức

Trong trường hợp của chúng ta, việc triển khai phương thức này khá đơn giản. Chúng tôi sẽ giả định rằng địa chỉ được căn chỉnh đúng cách và không kiểm tra lỗi bổ sung là bắt buộc.

Trước tiên, từ lệnh phải được tìm nạp từ bộ nhớ bằng cách sử dụng bộ nhớ giao diện và thực thể DataBuffer.

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

Tiếp theo, chúng ta gọi vào thực thể RiscVIEncoding để phân tích cú pháp từ hướng dẫn. bạn phải thực hiện việc này trước khi gọi chính bộ giải mã ISA. Xin nhắc lại rằng ISA các lệnh gọi bộ giải mã vào thực thể RiscVIEncoding trực tiếp để lấy mã hoạt động và toán hạng do từ lệnh chỉ định. Chúng tôi chưa triển khai tính năng đó nhưng hãy dùng void ParseInstruction(uint32_t) làm phương thức đó.

  riscv_encoding_->ParseInstruction(iword);

Cuối cùng, chúng ta gọi bộ giải mã ISA, truyền địa chỉ và lớp Encoding.

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

Nếu bạn cần trợ giúp (hoặc muốn kiểm tra bài tập của mình), câu trả lời đầy đủ là tại đây.


Lớp mã hoá

Lớp mã hoá triển khai một giao diện mà lớp bộ giải mã sử dụng để lấy opcode lệnh, toán hạng nguồn và toán hạng đích, và toán hạng tài nguyên. Tất cả các đối tượng này phụ thuộc vào thông tin từ tệp nhị phân bộ giải mã định dạng, chẳng hạn như mã vận hành, giá trị của các trường cụ thể trong từ chỉ dẫn, v.v. Lớp này được tách riêng với lớp bộ giải mã để giữ khả năng mã hoá độc lập và cho phép hỗ trợ nhiều lược đồ mã hoá trong tương lai.

RiscV32IEncodingBase là một lớp trừu tượng. Tập hợp phương pháp chúng ta phải triển khai trong lớp dẫn xuất của chúng tôi được hiển thị dưới đây.

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;
};

Thoạt nhìn thì có vẻ hơi phức tạp, đặc biệt là với số lượng nhưng đối với một kiến trúc đơn giản như RiscV, chúng tôi thực sự bỏ qua hầu hết các tham số, như giá trị của chúng sẽ được ngụ ý.

Hãy lần lượt tìm hiểu từng phương pháp.

OpcodeEnum GetOpcode(SlotEnum slot, int entry);

Phương thức GetOpcode trả về thành phần OpcodeEnum của giá trị hiện tại lệnh, xác định mã hoạt động của lệnh. Lớp OpcodeEnum là được xác định trong tệp isa giải mã riscv32i_enums.h đã tạo. Phương thức này sẽ lấy hai tham số, cả hai đều có thể bị bỏ qua. Đầu tiên trong số đây là loại ô (một lớp enum cũng được xác định trong riscv32i_enums.h), do RiscV chỉ có một vị trí duy nhất nên chỉ có một giá trị hợp lệ: SlotEnum::kRiscv32 Thứ hai là số thực thể của vùng (trong trường hợp có nhiều lần xuất hiện của vị trí, có thể xảy ra trong một số VLIW cấu trúc).

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);

Hai phương pháp tiếp theo được dùng để lập mô hình tài nguyên phần cứng trong bộ xử lý để cải thiện độ chính xác của chu kỳ. Đối với các bài tập hướng dẫn, chúng tôi sẽ không sử dụng nên trong quá trình triển khai, chúng sẽ bị loại bỏ và trả về 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);

Ba phương thức này trả về con trỏ đến các đối tượng toán hạng được dùng trong các hàm ngữ nghĩa lệnh để truy cập vào giá trị của bất kỳ lệnh nào phân tích toán hạng vị từ, từng toán hạng nguồn lệnh và viết mới cho các toán hạng đích của lệnh. Vì RiscV không sử dụng theo vị ngữ lệnh, thì phương thức đó chỉ cần trả về nullptr.

Mẫu tham số tương tự nhau trong các hàm này. Đầu tiên, giống như GetOpcode ô và mục nhập được truyền vào. Sau đó, mã vận hành cho lệnh mà toán hạng phải được tạo. Tính năng này chỉ được sử dụng nếu các mã hoạt động khác nhau cần trả về các đối tượng toán hạng khác nhau cho cùng một toán hạng mà chúng tôi không áp dụng cho trình mô phỏng RiscV này.

Tiếp theo là mục Thuộc tính, Nguồn và Đích đến, mục liệt kê toán hạng xác định toán hạng phải được tạo. Đây là 3 OpEnums trong riscv32i_enums.h như bên dưới:

  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,
  };

Nếu nhìn lại riscv32.isa , bạn sẽ lưu ý rằng chúng tương ứng với các tập hợp nguồn và đích tên toán hạng dùng trong phần khai báo của mỗi lệnh. Bằng cách sử dụng các tên toán hạng cho các toán hạng đại diện cho các trường bit và toán hạng khác nhau loại này, nó giúp việc viết lớp mã hoá dễ dàng hơn dưới dạng thành phần enum duy nhất xác định chính xác kiểu toán hạng cần trả về và không cần thiết hãy xem xét các giá trị của tham số vị trí, mục nhập hoặc mã hoạt động.

Cuối cùng, đối với toán hạng nguồn và đích, vị trí thứ tự của toán hạng toán hạng được truyền vào (một lần nữa, chúng ta có thể bỏ qua điều này) và đối với đích đến toán hạng, độ trễ (trong chu kỳ) trôi qua giữa thời điểm lệnh được phát hành và kết quả đích đến được cung cấp cho các hướng dẫn tiếp theo. Trong trình mô phỏng của chúng tôi, độ trễ này sẽ là 0, nghĩa là lệnh viết kết quả được gửi ngay đến sổ đăng ký.

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

Hàm cuối cùng dùng để lấy độ trễ của một đích đến cụ thể toán hạng nếu toán hạng đó được chỉ định là * trong tệp .isa. Trường hợp này không phổ biến, và không được dùng cho trình mô phỏng RiscV này, vì vậy, việc triển khai hàm này của chúng tôi sẽ chỉ trả về 0.


Xác định lớp mã hoá

Tệp tiêu đề (.h)

Phương thức

Mở tệp riscv32i_encoding.h. Tất cả những thông tin cần thiết, bao gồm cả tệp có đã được thêm và không gian tên đã được thiết lập. Tất cả thao tác thêm mã đã hoàn tất sau khi theo dõi nhận xét // Exercise 2.

Hãy bắt đầu bằng cách xác định một lớp RiscV32IEncoding kế thừa từ tạo giao diện.

class RiscV32IEncoding : public RiscV32IEncodingBase {
 public:

};

Tiếp theo, hàm khởi tạo phải đưa con trỏ đến thực thể trạng thái, trong trường hợp này một con trỏ đến riscv::RiscVState. Bạn nên sử dụng hàm huỷ mặc định.

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

Trước khi thêm tất cả các phương thức giao diện, hãy thêm phương thức được gọi bằng RiscV32Decoder để phân tích cú pháp lệnh:

void ParseInstruction(uint32_t inst_word);

Tiếp theo, hãy thêm các phương thức có cơ chế ghi đè không đáng kể trong khi bỏ tên của các thông số không được sử dụng:

// 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; }

Cuối cùng, thêm các ghi đè phương thức còn lại của giao diện công khai nhưng với việc triển khai được trì hoãn đến tệp .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;

Để đơn giản hoá việc triển khai từng phương thức getter toán hạng chúng ta sẽ tạo hai mảng callables (đối tượng hàm) được lập chỉ mục bởi giá trị số của thành phần SourceOpEnumDestOpEnum tương ứng. Bằng cách này, phần nội dung của các lớp này của các phương thức được giảm xuống để gọi hàm đối tượng hàm cho giá trị enum được truyền vào và trả về giá trị trả về giá trị.

Để sắp xếp việc khởi tạo 2 mảng này, chúng ta xác định 2 phương thức sẽ được gọi từ hàm khởi tạo như sau:

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

Thành viên dữ liệu

Sau đây là những thành phần dữ liệu cần có:

  • state_ để lưu giá trị riscv::RiscVState *.
  • inst_word_ thuộc loại uint32_t chứa giá trị của dòng từ chỉ dẫn.
  • opcode_ để lưu mã hoạt động của lệnh hiện tại được cập nhật bằng phương thức ParseInstruction. Tham số này có kiểu OpcodeEnum.
  • source_op_getters_ một mảng để lưu trữ các lệnh gọi dùng để lấy nguồn các đối tượng toán hạng. Loại của phần tử mảng là absl::AnyInvocable<SourceOperandInterface *>()>.
  • dest_op_getters_ một mảng để lưu trữ các hàm gọi dùng để lấy các đối tượng toán hạng đích. Loại của phần tử mảng là absl::AnyInvocable<DestinationOperandInterface *>()>.
  • xreg_alias một mảng tên ABI của thanh ghi số nguyên RiscV, ví dụ: "không" và "ra" thay vì "x0" và "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"};

Nếu bạn cần trợ giúp (hoặc muốn kiểm tra bài tập của mình), câu trả lời đầy đủ là tại đây.

Tệp nguồn (.cc).

Mở tệp riscv32i_encoding.cc. Tất cả những thông tin cần thiết, bao gồm cả tệp có đã được thêm và không gian tên đã được thiết lập. Tất cả thao tác thêm mã đã hoàn tất sau khi theo dõi nhận xét // Exercise 2.

Chức năng trợ giúp

Chúng ta sẽ bắt đầu bằng cách viết một số hàm trợ giúp dùng để tạo toán hạng của thanh ghi nguồn và đích. Các báo cáo này sẽ được tạo mẫu trên đăng ký loại đăng ký và sẽ gọi đối tượng RiscVState để xử lý đăng ký đối tượng rồi gọi phương thức factory của toán hạng trong đối tượng thanh ghi.

Hãy bắt đầu với trình trợ giúp toán hạng đích:

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);
}

Như bạn có thể thấy, có 2 hàm trợ giúp. Bước thứ hai thực hiện thêm tham số op_name cho phép toán hạng có tên hoặc chuỗi khác khác với thanh ghi cơ bản.

Tương tự như vậy đối với trình trợ giúp toán hạng nguồn:

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;
}

Hàm giao diện và hàm khởi tạo

Hàm khởi tạo và các hàm giao diện rất đơn giản. Hàm khởi tạo chỉ gọi 2 phương thức khởi tạo để khởi tạo các mảng callables cho phương thức getter của toán hạng.

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

ParseInstruction lưu trữ từ hướng dẫn rồi mã hoạt động tương ứng lấy được từ việc gọi vào mã do bộ giải mã nhị phân tạo.

// 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_);
}

Cuối cùng, các phương thức getter toán hạng sẽ trả về giá trị từ hàm getter mà nó gọi dựa trên tra cứu mảng bằng cách sử dụng giá trị enum toán hạng nguồn/đích.


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)]();
}

Phương thức khởi tạo mảng

Như bạn có thể đoán, hầu hết công việc là khởi tạo getter nhưng đừng lo, bạn có thể thực hiện việc này bằng một mẫu đơn giản, lặp lại. Hãy bắt đầu bằng InitializeDestinationOpGetters() trước, vì chỉ có một một vài toán hạng đích.

Hãy gọi lại lớp DestOpEnum được tạo từ riscv32i_enums.h:

  enum class DestOpEnum {
    kNone = 0,
    kCsr = 1,
    kNextPc = 2,
    kRd = 3,
    kPastMaxValue = 4,
  };

Đối với dest_op_getters_, chúng ta cần khởi tạo 4 mục nhập, mỗi mục cho kNone, kCsr, kNextPckRd. Để thuận tiện, mỗi mục nhập được khởi tạo bằng một lambda, mặc dù bạn cũng có thể sử dụng bất kỳ dạng lệnh gọi nào khác. Chữ ký của hàm lambda là void(int latency).

Cho đến bây giờ, chúng tôi chưa nói nhiều về các loại điểm đến khác nhau toán hạng được xác định trong MPACT-Sim. Đối với bài tập này, chúng ta sẽ chỉ sử dụng hai loại: generic::RegisterDestinationOperand được xác định trong register.h, và generic::DevNullOperand được xác định trong devnull_operand.h. Hiện tại, chi tiết của các toán hạng này không thực sự quan trọng, ngoại trừ việc phần đầu dùng để ghi vào các thanh ghi còn phần sau bỏ qua tất cả các lượt ghi.

Mục nhập đầu tiên cho kNone không đáng kể – chỉ cần trả về một giá trị nullptr và không bắt buộc ghi một lỗi.

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

Tiếp theo là tin kCsr. Ở đây, chúng ta sẽ ăn gian một chút. "Hello world" (Xin chào mọi người) chương trình không dựa vào bất kỳ bản cập nhật CSR thực tế nào, nhưng có một số mã nguyên mẫu thực thi các lệnh CSR. Giải pháp là chỉ giả nguỵ trang này bằng cách sử dụng sổ đăng ký thông thường có tên là "CSR" và chuyển tất cả những nội dung như vậy vào kênh.

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

Tiếp theo là kNextPc, nghĩa là "máy tính" thanh ghi. Mục tiêu này được dùng làm mục tiêu cho tất cả hướng dẫn chuyển nhánh và di chuyển. Tên được định nghĩa trong RiscVStatekPcName

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

Cuối cùng là toán hạng đích kRd. Trong riscv32i.isa, toán hạng rd chỉ dùng để tham chiếu đến thanh ghi số nguyên được mã hoá trong phần "rd" trường từ hướng dẫn, nên không có sự mơ hồ đối với từ hướng dẫn. Có chỉ là một chức năng. Đăng ký x0 (tên abi zero) được kết nối trực tiếp thành 0, Vì vậy, đối với thanh ghi đó, chúng ta sẽ sử dụng DevNullOperand.

Vì vậy, trong getter này, trước tiên, chúng ta trích xuất giá trị trong trường rd bằng cách sử dụng Phương thức Extract được tạo từ tệp .bin_fmt. Nếu giá trị bằng 0, chúng ta trả về lỗi "DevNull" toán hạng, nếu không, chúng ta sẽ trả về đúng toán hạng đăng ký, hãy nhớ sử dụng bí danh đăng ký thích hợp làm tên toán hạng.

  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]);
    )
  }
}

Bây giờ, hãy xem phương thức InitializeSourceOperandGetters(), trong đó mẫu là giống nhau nhiều, nhưng thông tin chi tiết sẽ hơi khác nhau.

Trước tiên, hãy xem SourceOpEnum được tạo từ riscv32i.isa trong hướng dẫn đầu tiên:

  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,
  };

Việc kiểm tra các thành viên, ngoài kNone, họ chia thành hai nhóm. Một là các toán hạng tức thì: kBimm12, kImm12, kJimm20, kSimm12, kUimm20, và kUimm5. Còn lại là các toán hạng đăng ký: kCsr, kRs1kRs2.

Toán hạng kNone được xử lý giống như toán hạng đích – trả về một chân ái

void RiscV32IEncoding::InitializeSourceOperandGetters() {
  // Source operand getters.
  source_op_getters_[static_cast<int>(SourceOpEnum::kNone)] = [] () {
    return nullptr;
  };

Tiếp theo, hãy thực hiện về toán hạng thanh ghi. Chúng tôi sẽ xử lý kCsr tương tự về cách chúng tôi xử lý các toán hạng đích tương ứng – chỉ cần gọi chức năng trợ giúp bằng cách sử dụng "CSR" làm tên sổ đăng ký.

  // Register operands.
  source_op_getters_[static_cast<int>(SourceOpEnum::kCsr)] = [this]() {
    return GetRegisterSourceOp<RV32Register>(state_, "CSR");
  };

Toán hạng kRs1kRs2 được xử lý tương đương với kRd, ngoại trừ việc mặc dù chúng tôi không muốn cập nhật x0 (hoặc zero), nhưng chúng tôi vẫn muốn đảm bảo rằng chúng ta luôn đọc 0 từ toán hạng đó. Để làm được điều đó, chúng tôi sẽ sử dụng Lớp generic::IntLiteralOperand<> được xác định trong literal_operand.h. Toán hạng này được dùng để lưu trữ một giá trị cố định (trái ngược với một giá trị mô phỏng giá trị tức thì). Nếu không, mẫu sẽ giống nhau: trước tiên hãy trích xuất mẫu Giá trị rs1/rs2 của từ lệnh, nếu giá trị bằng 0 thì hãy trả về giá trị bằng chữ toán hạng có tham số mẫu 0, nếu không sẽ trả về một thanh ghi thông thường toán hạng nguồn bằng hàm trợ giúp, trong đó bí danh abi làm toán hạng .

  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]);
  };

Cuối cùng, chúng ta xử lý các toán hạng tức thì khác nhau. Giá trị tức thì là được lưu trữ trong các thực thể của lớp generic::ImmediateOperand<> được xác định trong immediate_operand.h. Sự khác biệt duy nhất giữa các phương thức getter khác nhau cho các toán hạng tức thì là chức năng Trích xuất nào được sử dụng và liệu loại bộ nhớ đã được ký hay chưa có chữ ký, theo trường bit.

  // Immediates.
  source_op_getters_[static_cast<int>(SourceOpEnum::kBimm12)] = [this]() {
    return new ImmediateOperand<int32_t>(
        inst32_format::ExtractBImm(inst_word_));
  };
  source_op_getters_[static_cast<int>(SourceOpEnum::kImm12)] = [this]() {
    return new ImmediateOperand<int32_t>(
        inst32_format::ExtractImm12(inst_word_));
  };
  source_op_getters_[static_cast<int>(SourceOpEnum::kUimm5)] = [this]() {
    return new ImmediateOperand<uint32_t>(
        inst32_format::ExtractUimm5(inst_word_));
  };
  source_op_getters_[static_cast<int>(SourceOpEnum::kJimm20)] = [this]() {
    return new ImmediateOperand<int32_t>(
        inst32_format::ExtractJImm(inst_word_));
  };
  source_op_getters_[static_cast<int>(SourceOpEnum::kSimm12)] = [this]() {
    return new ImmediateOperand<int32_t>(
        inst32_format::ExtractSImm(inst_word_));
  };
  source_op_getters_[static_cast<int>(SourceOpEnum::kUimm20)] = [this]() {
    return new ImmediateOperand<uint32_t>(
        inst32_format::ExtractUimm32(inst_word_));
  };
}

Nếu bạn cần trợ giúp (hoặc muốn kiểm tra bài tập của mình), câu trả lời đầy đủ là tại đây.

Bài viết này sẽ kết thúc hướng dẫn này. Chúng tôi hy vọng thông tin này hữu ích.