Zintegrowany dekoder RiscV

Cele tego samouczka to:

  • Dowiedz się, jak wygenerowane dekodery ISA i dekodery binarne współpracują ze sobą.
  • Napisz niezbędny kod w C++, aby utworzyć pełny dekoder instrukcji dla RiscV. RV32I, który łączy dekodery ISA i dekodery binarne.

Korzystanie z dekodera instrukcji

Dekoder instrukcji odpowiada za odczytanie, pod kątem adresu instrukcji, słowa instrukcji z pamięci i zwraca w pełni zainicjowaną instancję Instruction, który reprezentuje tę instrukcję.

Dekoder najwyższego poziomu implementuje poniższy kod generic::DecoderInterface:

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

Jak widać, należy wdrożyć tylko jedną metodę: cpp virtual Instruction *DecodeInstruction(uint64_t address);

Przyjrzyjmy się teraz, co zawiera wygenerowany kod, a co jest potrzebne.

Najpierw zwróć uwagę na klasę najwyższego poziomu RiscV32IInstructionSet w pliku riscv32i_decoder.h, który został wygenerowany na koniec samouczka w dniu dekodera ISA. Aby ponownie zobaczyć zawartość, przejdź do katalogu rozwiązań i stworzyć wszystko od nowa.

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

Zmień katalog z powrotem na katalog główny repozytorium i sprawdźmy, w wygenerowanych źródłach. Aby to zrobić, zmień katalog na bazel-out/k8-fastbuild/bin/riscv_isa_decoder (zakładając, że używasz procesora x86 host – w przypadku innych hostów kolejnym ciągiem znaków będzie k8-fastbuild).

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

Zobaczysz listę czterech plików źródłowych zawierających wygenerowany kod w C++:

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

Otwórz pierwszy plik riscv32i_decoder.h. Istnieją 3 zajęcia, na które trzeba zwrócić uwagę:

  • RiscV32IEncodingBase
  • RiscV32IInstructionSetFactory
  • RiscV32IInstructionSet

Zwróć uwagę na nazwy klas. Nazwa wszystkich zajęć zależy od Wersja nazwy podana w polu „isa” (z uwzględnieniem alfabetu łacińskiego) w tym pliku: isa RiscV32I { ... }

Zacznijmy od zajęć RiscVIInstructionSet. Widać ją poniżej:

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

Na tych zajęciach nie ma metod wirtualnych, jest to więc klasa samodzielna, ale zauważyć 2 rzeczy. Najpierw konstruktor wskazuje na instancję RiscV32IInstructionSetFactory zajęcia. Jest to klasa wygenerowana przez za pomocą dekodera do utworzenia instancji klasy RiscV32Slot, która służy do zdekodować wszystkie instrukcje zdefiniowane dla obiektu slot RiscV32, jak określono w riscv32i.isa. Po drugie, metoda Decode pobiera dodatkowy parametr wskaźnika typu do RiscV32IEncodingBase, to klasa, która zwróci interfejsu między dekoderem isa wygenerowanym w pierwszym samouczku a plikiem binarnym za pomocą dekodera wygenerowanego w drugim module.

Klasa RiscV32IInstructionSetFactory jest klasą abstrakcyjną, z której własnego dekodera. W większości przypadków klasa jest prosta: podaj metodę wywoływania konstruktora dla każdego klasę przedziałów zdefiniowaną w pliku .isa. W naszym przypadku jest to bardzo proste, jest tylko jedną taką klasą: Riscv32Slot (pascal-case w nazwie riscv32 połączony z Slot). Metoda nie jest generowana, ponieważ w zaawansowanych przypadkach użycia, w których może być przydatne do pobierania podklasy. z boksu i wywoływać jego konstruktor.

W dalszej części tego szkolenia omówimy ostatnie zajęcia RiscV32IEncodingBase. bo to jest temat innego ćwiczenia.


Zdefiniuj dekoder instrukcji najwyższego poziomu

Zdefiniuj klasę fabryki

Jeśli projekt został ponownie skompilowany w ramach pierwszego samouczka, pamiętaj, aby przejść z powrotem na w katalogu riscv_full_decoder.

Otwórz plik riscv32_decoder.h. Wszystkie niezbędne pliki „Uwzględnij” zostały już dodano, a przestrzenie nazw zostały skonfigurowane.

Zdefiniuj zajęcia po komentarzu oznaczonym jako //Exercise 1 - step 1. Pole RiscV32IsaFactory odziedziczone z grupy RiscV32IInstructionSetFactory.

class RiscV32IsaFactory : public RiscV32InstructionSetFactory {};

Następnie zdefiniuj zastąpienie dla CreateRiscv32Slot. Ponieważ nie używamy żadnych klasy derywowanej klasy Riscv32Slot, po prostu przydzielamy nową instancję za pomocą argumentu std::make_unique

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

Jeśli potrzebujesz pomocy (lub chcesz sprawdzić swoje zadanie), pełna odpowiedź to tutaj.

Zdefiniuj klasę dekodera

Deklaracje konstruktorów, destruktorów i metod

Teraz trzeba zdefiniować klasę dekodera. W tym samym pliku co powyżej otwórz deklaracji RiscV32Decoder. Rozwiń deklarację do definicji klasy gdzie RiscV32Decoder dziedziczy dane z generic::DecoderInterface.

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

Następnie, zanim napiszemy konstruktor, przyjrzyjmy się kodowi które wygenerujemy w drugim samouczku na dekoderze plików binarnych. Oprócz wszystkich Extract, istnieje funkcja DecodeRiscVInst32:

OpcodeEnum DecodeRiscVInst32(uint32_t inst_word);

Ta funkcja bierze pod uwagę słowo instrukcji, które musi zostać zdekodowane, i zwraca zgodnie z instrukcją. Z drugiej strony Klasa DecodeInterface, którą RiscV32Decoder implementuje tylko karty w adresu. Dlatego klasa RiscV32Decoder musi mieć dostęp do pamięci, przeczytaj słowo z instrukcji do: DecodeRiscVInst32(). W tym projekcie dostępu do pamięci jest oparty na prostym interfejsie pamięci określonym .../mpact/sim/util/memory o odpowiedniej nazwie util::MemoryInterface, zobacz poniżej:

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

Dodatkowo musimy mieć możliwość przekazania instancji klasy state do funkcji konstruktory innych klas dekodera. Odpowiednia klasa stanu to Klasa riscv::RiscVState, która pochodzi od: generic::ArchState, z dodanym dla RiscV. Oznacza to, że musimy zadeklarować konstruktor, aby może ustawić wskaźnik do state i memory:

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

Usuń domyślny konstruktor i zastąp destruktor:

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

Następnie zadeklaruj metodę DecodeInstruction, którą musimy zastąpić generic::DecoderInterface

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

Jeśli potrzebujesz pomocy (lub chcesz sprawdzić swoje zadanie), pełna odpowiedź to tutaj.


Definicje użytkowników danych

Klasa RiscV32Decoder będzie potrzebować prywatnych członków danych, aby przechowywać z parametrami konstruktora i wskaźnikiem do klasy fabryki.

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

Wymaga też wskaźnika do klasy kodowania, która pochodzi z RiscV32IEncodingBase, nazwijmy ją RiscV32IEncoding (wprowadzimy w ćwiczeniu 2). Dodatkowo wymaga wskaźnika do instancji RiscV32IInstructionSet, więc dodaj:

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

Na koniec musimy zdefiniować użytkownika danych do użytku w naszym interfejsie pamięci:

  generic::DataBuffer *inst_db_;

Jeśli potrzebujesz pomocy (lub chcesz sprawdzić swoje zadanie), pełna odpowiedź to tutaj.

Zdefiniuj metody klasy dekodera

Następnie trzeba wdrożyć konstruktor, destruktor Metoda DecodeInstruction. Otwórz plik riscv32_decoder.cc. Wartość jest pusta są już w pliku, a także deklaracje przestrzeni nazw i kilka metod z using deklaracji.

Definicja konstruktora

Konstruktor musi tylko zainicjować elementy danych. Najpierw zainicjuj state_ i memory_:

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

Następnie przydziel instancje każdej z klas powiązanych z dekoderem, przekazując w poleceniu za pomocą odpowiednich parametrów.

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

Na koniec przydziel instancję DataBuffer. Jest przydzielany z użyciem fabryki dostępne w ramach usługi state_. Przydzielamy bufor danych o rozmiarze umożliwiającym przechowywanie pojedynczy uint32_t, bo taki jest rozmiar słowa instrukcji.

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

Definicja destruktora

Destruktor jest prosty: wystarczy zwolnić obiekty przydzielone w konstruktorze, ale jednym zakręciem. Instancja bufora danych jest liczona odniesieniem, więc zamiast tego przy wywołaniu delete dla tego wskaźnika DecRef() obiekt:

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

Definicja metody

W naszym przypadku wdrożenie tej metody jest dość proste. Przyjmijmy, że adres jest prawidłowo dopasowany i nie są sprawdzane żadne dodatkowe błędy.

Najpierw słowo instrukcji musi zostać pobrane z pamięci przy użyciu pamięci. interfejsu i instancji DataBuffer.

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

Następnie wywołujemy wystąpienie RiscVIEncoding, aby przeanalizować słowo instrukcji, co trzeba zrobić przed wywołaniem dekodera ISA. Pamiętaj, że agencja ISA dekoder wywołuje bezpośrednio instancję RiscVIEncoding, aby uzyskać kod op i operandów określonych przez słowo instruktażowe. Nie stosujemy jeszcze tej metody jeszcze nie ma klasy, ale użyjmy void ParseInstruction(uint32_t) jako metody tej metody.

  riscv_encoding_->ParseInstruction(iword);

Na koniec wywołujemy dekoder ISA, przekazując adres i klasę kodowania.

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

Jeśli potrzebujesz pomocy (lub chcesz sprawdzić swoje zadanie), pełna odpowiedź to tutaj.


Klasa kodowania

Klasa kodowania implementuje interfejs używany przez klasę dekodera uzyskać kod operacji instrukcji oraz jej operandy źródłowe i docelowe; operandów zasobów. Wszystkie te obiekty zależą od informacji z pliku binarnego z dekodera formatu, takiego jak kod operacji, wartości określonych pól w funkcji słowa instrukcji itp. Jest ona oddzielona od klasy dekodera, aby została niezależne od kodowania i obsługujące wiele różnych schematów kodowania w przyszłości.

RiscV32IEncodingBase to klasa abstrakcyjna. Zestaw metod, które stosujemy, można zastosować do klasy derywowanej poniżej.

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

Na pierwszy rzut oka wydaje się to dość skomplikowane, zwłaszcza przy ale w przypadku prostej architektury, takiej jak RiscV, ignorujemy większość ponieważ ich wartości będą domniemane.

Omówmy po kolei wszystkie metody.

OpcodeEnum GetOpcode(SlotEnum slot, int entry);

Metoda GetOpcode zwraca element OpcodeEnum dla bieżącego określający kod opcyjny instrukcji. Zajęcia OpcodeEnum to zdefiniowane w wygenerowanym pliku dekodera isa riscv32i_enums.h. Metoda dwóch parametrów, z których każdy może być ignorowany. Pierwsza wartość to jest typ przedziału (klasa wyliczeniowa zdefiniowana w riscv32i_enums.h), który, ponieważ RiscV ma tylko jeden boks, ma tylko jedną możliwą wartość: SlotEnum::kRiscv32 Drugi to numer instancji przedziału (w przypadku występuje wiele wystąpień przedziału, co może wystąpić w niektórych VLIW. architektury).

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

Następne 2 metody są używane do modelowania zasobów sprzętowych procesora aby poprawić dokładność cyklu. W ćwiczeniach instruktażowych nie będziemy używać więc w implementacji zostaną one skrócone, zwracając 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);

Te trzy metody zwracają wskaźniki do obiektów operand używanych w obrębie funkcji semantycznych instrukcji w celu uzyskania dostępu do wartości dowolnej instrukcji. operand predykatu, każdego operandu źródła instrukcji i wpisać nowy do operandów miejsca docelowego instrukcji. Ponieważ RiscV nie używa predykatów instrukcji, ta metoda musi zwracać tylko nullptr.

Wzorzec parametrów jest podobny we wszystkich tych funkcjach. Po pierwsze, podobnie jak GetOpcode boks i wpis są przekazywane. Następnie kod operacji dla operatora instrukcja, dla której ma zostać utworzony operand. Jest ona używana tylko wtedy, gdy różne kody operacji muszą zwracać różne obiekty operandu dla tego samego operandu co nie jest możliwe w przypadku tego symulatora RiscV.

Dalej mamy wpis predykat, źródło, miejsce docelowe i wyliczenie operandów, które wskazuje operand, który ma zostać utworzony. Pochodzą one z 3 grup, OpEnums w riscv32i_enums.h, tak jak poniżej:

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

Jeśli spojrzysz na dane riscv32.isa pliku, zauważysz, że odpowiadają one zbiorom danych źródłowych i docelowych nazwy operandów używanych w deklaracji poszczególnych instrukcji. Zastosowanie różnych opcji nazwy operandów operandów, które reprezentują różne pola bitowe i operandy typów kodu, ułatwia to pisanie klasy kodowania, ponieważ element enum jest unikalny określa dokładny typ operandu do zwrócenia, dzięki czemu nie trzeba uwzględnij wartości parametrów przedziału, wpisu lub kodu operacji.

W przypadku operandów źródłowych i docelowych jest to również położenie porządkowe funkcji operand jest przekazywany (znowu możemy go zignorować), a dla elementu docelowego operand, czas oczekiwania (w cyklach) upływający między momentem zastosowania instrukcji a wynik docelowy jest dostępny dla kolejnych instrukcji. W naszym symulatorze to opóźnienie wynosi 0, co oznacza, że instrukcja zapisuje wyniki są natychmiast przekazywane do rejestru.

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

Ostatnia funkcja służy do pobierania czasu oczekiwania dla danego miejsca docelowego. operand, jeśli został określony w pliku .isa jako *. Jest to rzadkie zjawisko, i nie jest używana w tym symulatorze RiscV, więc nasza implementacja tej funkcji zwróci po prostu 0.


Zdefiniuj klasę kodowania

Plik nagłówka (.h)

Metody

Otwórz plik riscv32i_encoding.h. Wszystkie niezbędne pliki „Uwzględnij” zostały już dodano, a przestrzenie nazw zostały skonfigurowane. Całe dodawanie kodu to użytkownik obserwuje komentarz // Exercise 2.

Zacznijmy od zdefiniowania klasy RiscV32IEncoding, która dziedziczy z klasy za pomocą wygenerowanego interfejsu.

class RiscV32IEncoding : public RiscV32IEncodingBase {
 public:

};

Następnie konstruktor powinien wskazać wskaźnik do instancji stanu (w tym przypadku będzie to stan) wskaźnik do riscv::RiscVState. Należy użyć domyślnego niszczyciela.

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

Zanim dopiszemy wszystkie metody interfejsu, dodajmy metodę wywoływaną przez RiscV32Decoder, aby przeanalizować instrukcję:

void ParseInstruction(uint32_t inst_word);

Teraz dodajmy metody, które mają proste zastąpienia i pozbądźmy nazwy nieużywanych parametrów:

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

Na koniec dodaj pozostałe zastąpienia metody interfejsu publicznego, ale z wdrożeniami odroczonymi do pliku .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;

Aby uprościć implementację poszczególnych metod pobierania operandów utworzymy dwie tablice wywołań (obiektów funkcji) indeksowanych przez metodę wartości liczbowe odpowiednio SourceOpEnum i DestOpEnum. W ten sposób formy są zredukowane do wywoływania obiekt funkcji dla wartości wyliczenia, która jest przekazywana i zwracająca zwracaną wartość .

Aby zorganizować inicjalizację tych dwóch tablic, definiujemy 2 prywatne które są wywoływane z konstruktora w następujący sposób:

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

Użytkownicy danych

Wymagani użytkownicy danych:

  • state_, aby przechowywać wartość riscv::RiscVState *.
  • inst_word_ typu uint32_t, który zawiera wartość bieżącej słowo instruktażowe.
  • opcode_, aby przechowywać kod operacji bieżącej instrukcji, który jest zaktualizowany przez metody ParseInstruction. Ten typ zasobu zawiera: OpcodeEnum.
  • source_op_getters_ tablica do przechowywania wywołań używanych do uzyskania źródła obiektów operandu. Typ elementów tablicy to absl::AnyInvocable<SourceOperandInterface *>()>
  • dest_op_getters_ tablica do przechowywania wywołań używanych do uzyskania obiekty operandu docelowego. Typ elementów tablicy to absl::AnyInvocable<DestinationOperandInterface *>()>
  • xreg_alias tablica nazw ABI rejestru liczb całkowitych RiscV, np. „zero” oraz „ra” zamiast „x0” i „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"};

Jeśli potrzebujesz pomocy (lub chcesz sprawdzić swoje zadanie), pełna odpowiedź to tutaj.

Plik źródłowy (.cc).

Otwórz plik riscv32i_encoding.cc. Wszystkie niezbędne pliki „Uwzględnij” zostały już dodano, a przestrzenie nazw zostały skonfigurowane. Całe dodawanie kodu to użytkownik obserwuje komentarz // Exercise 2.

Funkcje pomocnicze

Zaczniemy od opisania kilku funkcji pomocniczych, których używamy do tworzenia operandy rejestru źródłowego i docelowego. Zostaną one utworzone na podstawie rejestru i wywołuje obiekt RiscVState, aby pobrać uchwyt dla rejestrować obiekt, a następnie wywoływać metodę fabryki operandów w obiekcieregister.

Zacznijmy od pomocników docelowego operandu:

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

Jak widać, istnieją 2 funkcje pomocnicze. Drugi etap wymaga dodatkowych parametr op_name, który do operandu może mieć inną nazwę lub inny ciąg znaków niż w rejestrze bazowym.

Podobnie w przypadku pomocników operandu źródłowego:

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

Funkcje konstruktora i interfejsu

Funkcje konstruktora i interfejsu są bardzo proste. Konstruktor wywołuje tylko 2 metody inicjowania tablic callables dla pobierających operandy.

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

ParseInstruction przechowuje słowo instrukcji, a następnie podany kod operacji uzyskanych dzięki wywołaniu kodu wygenerowanego przez dekoder binarny.

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

Moduły pobierające operandy zwracają wartość wywołaną przez funkcję pobierania .


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

Metody inicjowania tablicy

Jak możesz się domyślić, większość pracy polega na inicjowaniu narzędzia ale bez obaw – robi się to przy użyciu łatwego, powtarzającego się wzorca. Zacznijmy zaczyna się od InitializeDestinationOpGetters(), ponieważ istnieje tylko kilka operandów miejsca docelowego.

Wycofaj wygenerowaną klasę DestOpEnum z riscv32i_enums.h:

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

W przypadku dest_op_getters_ musimy zainicjować 4 wpisy, po jednym dla kNone, kCsr, kNextPc i kRd. Dla wygody każdy wpis jest inicjowany ciągiem lambda, ale możesz też użyć dowolnej innej formy wywołania. Podpis wartości lambda wynosi void(int latency).

Do tej pory nie mówiliśmy zbyt wiele o różnych rodzajach miejsc docelowych, operandy zdefiniowane w formacie MPACT-Sim. W tym ćwiczeniu użyjemy tylko dwóch typy: generic::RegisterDestinationOperand zdefiniowane w register.h, i generic::DevNullOperand zdefiniowane w devnull_operand.h Szczegóły tych operandów nie są obecnie szczególnie ważne, z wyjątkiem tego, że pierwszy jest używany do zapisu w rejestrach, a drugi ignoruje wszystkie zapisy.

Pierwsza pozycja dla kNone jest trywialna – wystarczy zwrócić wartość nullptr i opcjonalnie zarejestrować błąd.

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

Kolejny serwis to kCsr. Zrobimy tu trochę oszustw. „Witaj świecie” program nie wymaga faktycznej aktualizacji żądania podpisania certyfikatu, ale zawiera wykonaj instrukcje żądania podpisania certyfikatu. Rozwiązaniem jest pozbycie się tego problemu za pomocą zwykły rejestr o nazwie „CSR” i kierować do niego wszystkie tego typu zapisy.

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

Następna jest nazwa kNextPc i odnosi się do nazwy „PC” rejestracji. Jest używana jako wartość docelowa dla wszystkich instrukcji dotyczących gałęzi i skoków. Nazwa jest określona w języku: RiscVState jako kPcName

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

Na koniec znajduje się operand miejsca docelowego kRd. W riscv32i.isa operand Parametr rd jest używany wyłącznie do odwoływania się do rejestru całkowitej zakodowanego w „rd” pole wyrazu instruktażowego, więc nie ma wątpliwości, do której się odnosi. OK to tylko jeden widżet. Rejestracja domeny x0 (nazwa abi zero) jest na stałe ustawiona na 0, W tym rejestrze korzystamy z DevNullOperand.

W tym narzędziu getter najpierw wyodrębniamy wartość z pola rd za pomocą funkcji Metoda Extract wygenerowana z pliku .bin_fmt. Jeśli wartość wynosi 0, zwraca „DevNull”, operand. W przeciwnym razie zwracamy poprawny operand rejestru, pamiętając o użyciu odpowiedniego aliasu rejestru jako nazwy operandu.

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

Przejdźmy do metody InitializeSourceOperandGetters(), w której wzór prawie taki sam, ale szczegóły nieco się różnią.

Najpierw przyjrzymy się elementowi SourceOpEnum wygenerowanym na podstawie riscv32i.isa w pierwszym samouczku:

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

Oprócz grupy kNone członkowie dzielą się na 2 grupy. Jeden jest bezpośrednimi operandami: kBimm12, kImm12, kJimm20, kSimm12, kUimm20, i kUimm5. Drugie to operandy rejestru: kCsr, kRs1 i kRs2.

Argument kNone jest obsługiwany tak samo jak w przypadku operandów miejsca docelowego – zwracaj nullptr.

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

Teraz popracujemy nad operandami rejestru. Zajmiemy się kCsr podobnymi do sposobu obsługi odpowiednich operandów miejsca docelowego – po prostu wywołaj funkcję funkcja pomocnicza używająca żądania „CSR” jako nazwę rejestru.

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

Operatory kRs1 i kRs2 są obsługiwane tak samo jak kRd, z tym wyjątkiem nie chcieliśmy aktualizować rozszerzenia x0 (lub zero), ale chcemy upewnić się, zawsze odczytuje z tego operandu 0. Użyjemy do tego celu generic::IntLiteralOperand<> klasa zdefiniowana w literal_operand.h Ten operand służy do przechowywania wartości literału (w przeciwieństwie do symulowanej wartości wartość natychmiastową). Jeśli tego nie zrobisz, wzór będzie taki sam: najpierw wyodrębnij Wartość rs1/rs2 ze słowa instrukcji, jeśli wartość wynosi zero, zwraca literał operand z parametrem szablonu o wartości 0, w przeciwnym razie zwraca zwykły rejestr operand źródłowy przy użyciu funkcji pomocniczej z aliasem abi jako operandem imię i nazwisko.

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

Na koniec obsługujemy różne natychmiastowe operandy. Wartości natychmiastowe to przechowywane w instancjach klasy generic::ImmediateOperand<> zdefiniowanej w immediate_operand.h Jedyna różnica między różnymi metodami getter w przypadku bezpośrednich operandów. wskazuje, która funkcja wyodrębniania jest używana i czy typ pamięci masowej jest podpisany lub zgodnie z polem bitowym.

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

Jeśli potrzebujesz pomocy (lub chcesz sprawdzić swoje zadanie), pełna odpowiedź to tutaj.

To koniec samouczka. Mamy nadzieję, że te informacje okażą się przydatne.