Dekoder ISA RiscV

Cele tego samouczka to:

  • Dowiedz się, jak są przedstawiane instrukcje w symulatorze MPACT-Sim.
  • Poznaj strukturę i składnię pliku opisu ISA.
  • Utwórz opisy ISA dla podzbioru instrukcji RiscV RV32I

Omówienie

W formacie MPACT-Sim instrukcje docelowe są dekodowane i przechowywane w wewnętrznym udostępnianie informacji oraz semantykę które można szybciej wykonać. Te instancje instrukcji są przechowywane w pamięci podręcznej w instrukcji pamięci podręcznej, tak aby zmniejszyć liczbę często wykonywanych instrukcji .

Zajęcia instruktażowe

Zanim zaczniemy, przypomnijmy sobie, jak instrukcje przedstawione w formacie MPACT-Sim. Klasa Instruction jest zdefiniowana w: mpact-sim/mpact/sim/generic/instruction.h.

Instancja klasy instrukcji zawiera wszystkie informacje niezbędne do symulują instrukcję, gdy jest „wykonana”, na przykład:

  1. Adres instrukcji, symulowany rozmiar instrukcji, czyli rozmiar w formacie .text.
  2. Kod operacji instrukcji.
  3. Wskaźnik interfejsu predykatu (jeśli występuje).
  4. Wektor wskaźników interfejsu operandu źródłowego.
  5. Wektor wskaźników interfejsu operandu docelowego.
  6. Funkcja semantyczna może być wywoływana.
  7. Wskaźnik do obiektu stanu architektury.
  8. Wskaźnik do obiektu kontekstu.
  9. Wskaźnik do podrzędnych i kolejnych instancji instrukcji.
  10. Ciąg do zdemontowania.

Instancje te są zwykle przechowywane w pamięci podręcznej instrukcji (instancji). może być używany przy każdym ponownym wykonaniu instrukcji. Zwiększa to wydajność w trakcie działania.

Oprócz wskaźnika do obiektu kontekstu wszystkie te elementy są wypełniane dekoder instrukcji wygenerowany na podstawie opisu ISA. Do tego celu nie musisz znać szczegółowych informacji na temat tych elementów, ponieważ nie będziemy korzystając z nich bezpośrednio. Ogólne zrozumienie tego, jak z nich korzystamy, wystarczająca.

Wywoływana funkcja semantyczna to obiekt funkcji/metody/funkcji w C++. (w tym lambda), która stosuje semantykę instrukcji. Dla: dla instrukcji add wczytuje każdy operand źródłowy, dodaje dwa i zapisuje wynik w pojedynczym operandie miejsca docelowego. Temat funkcje semantyczne zostały szczegółowo omówione w samouczku funkcji semantycznych.

Argumenty instrukcji

Klasa instrukcji zawiera wskaźniki do 3 typów interfejsów operand: predykat, źródło i miejsce docelowe. Te interfejsy umożliwiają funkcjom semantycznym być napisana niezależnie od rzeczywistego typu instrukcji . Na przykład dostęp do wartości rejestrów i bezpośrednich jest wykonywany przy użyciu tego samego interfejsu. Oznacza to, że instrukcje wykonujące takie same działania ale na różnych operandach (np. rejestrach i immitacjach) może zostać za pomocą tej samej funkcji semantycznej.

Interfejs operandu predykatu dla tych ISA, które obsługują predykat wykonanie instrukcji (w przypadku innych identyfikatorów ISA ma wartość null) służy do określenia, czy dana instrukcja powinna być wykonywana na podstawie wartości logicznej predykatu.

// The predicte operand interface is intended primarily as the interface to
// read the value of instruction predicates. It is separated from source
// predicates to avoid mixing it in with the source operands needed for modeling
// the instruction semantics.
class PredicateOperandInterface {
 public:
  virtual bool Value() = 0;
  // Return a string representation of the operand suitable for display in
  // disassembly.
  virtual std::string AsString() const = 0;
  virtual ~PredicateOperandInterface() = default;
};

Interfejs operandu źródłowego umożliwia funkcji semantycznej instrukcji odczytywanie wartości z operandów instrukcji niezależnie od operandu typu. Metody interfejsu obsługują operandy o wartości skalarnej i wektorowej.

// The source operand interface provides an interface to access input values
// to instructions in a way that is agnostic about the underlying implementation
// of those values (eg., register, fifo, immediate, predicate, etc).
class SourceOperandInterface {
 public:
  // Methods for accessing the nth value element.
  virtual bool AsBool(int index) = 0;
  virtual int8_t AsInt8(int index) = 0;
  virtual uint8_t AsUint8(int index) = 0;
  virtual int16_t AsInt16(int index) = 0;
  virtual uint16_t AsUint16(int) = 0;
  virtual int32_t AsInt32(int index) = 0;
  virtual uint32_t AsUint32(int index) = 0;
  virtual int64_t AsInt64(int index) = 0;
  virtual uint64_t AsUint64(int index) = 0;

  // Return a pointer to the object instance that implements the state in
  // question (or nullptr) if no such object "makes sense". This is used if
  // the object requires additional manipulation - such as a fifo that needs
  // to be pop'ed. If no such manipulation is required, nullptr should be
  // returned.
  virtual std::any GetObject() const = 0;

  // Return the shape of the operand (the number of elements in each dimension).
  // For instance {1} indicates a scalar quantity, whereas {128} indicates an
  // 128 element vector quantity.
  virtual std::vector<int> shape() const = 0;

  // Return a string representation of the operand suitable for display in
  // disassembly.
  virtual std::string AsString() const = 0;

  virtual ~SourceOperandInterface() = default;
};

Interfejs docelowego operandu udostępnia metody przydzielania i obsługi DataBuffer instancji (wewnętrzny typ danych używany do przechowywania wartości rejestrów). O z operandem miejsca docelowego jest też związane opóźnienie, które jest liczbą cykli oczekiwania na pojawienie się bufora danych przydzielonego przez instrukcję służy do aktualizowania wartości rejestru docelowego. Dla: czas oczekiwania instrukcji add może wynosić 1, a dla mpy może być 4. Szczegółowo opisujemy to w funkcje semantyczne.

// The destination operand interface is used by instruction semantic functions
// to get a writable DataBuffer associated with a piece of simulated state to
// which the new value can be written, and then used to update the value of
// the piece of state with a given latency.
class DestinationOperandInterface {
 public:
  virtual ~DestinationOperandInterface() = default;
  // Allocates a data buffer with ownership, latency and delay line set up.
  virtual DataBuffer *AllocateDataBuffer() = 0;
  // Takes an existing data buffer, and initializes it for the destination
  // as if AllocateDataBuffer had been called.
  virtual void InitializeDataBuffer(DataBuffer *db) = 0;
  // Allocates and initializes data buffer as if AllocateDataBuffer had been
  // called, but also copies in the value from the current value of the
  // destination.
  virtual DataBuffer *CopyDataBuffer() = 0;
  // Returns the latency associated with the destination operand.
  virtual int latency() const = 0;
  // Return a pointer to the object instance that implmements the state in
  // question (or nullptr if no such object "makes sense").
  virtual std::any GetObject() const = 0;
  // Returns the order of the destination operand (size in each dimension).
  virtual std::vector<int> shape() const = 0;
  // Return a string representation of the operand suitable for display in
  // disassembly.
  virtual std::string AsString() const = 0;
};

Opis ISA

Architektura ISA (instrukcji zbioru instrukcji) procesora definiuje abstrakcyjny model które współdziała ze sprzętem. Definiuje on dostępne instrukcje, typy danych, rejestry i inne parametry maszyny działają instrukcje, a także ich działanie (semantykę). Cele: formatu MPACT-Sim, ISA nie zawiera rzeczywistego kodowania instrukcji. To jest traktowane oddzielnie.

Identyfikator ISA procesora jest zapisany w pliku z opisem, który opisuje na abstrakcyjnym, niezależnym poziomie kodowania. Plik z opisem wylicza zestaw dostępnych instrukcji. Instrukcja dla każdej instrukcji musi zawierać jego nazwę, liczbę i nazwy operandów oraz powiązanie z funkcją/wywołaniem w C++, które implementuje jego semantykę. Dodatkowo: można określić ciąg formatowania, a użycie instrukcji nazwy zasobów sprzętowych. Pierwszy przydaje się do tworzenia tekstu z instrukcją dotyczącą debugowania, śledzenia lub użytku interaktywnego. a ten drugi może zostać użyty do uzyskania większej dokładności cyklu w symulacji.

Plik z opisem ISA jest analizowany przez parser ISA, który generuje kod dla czyli niezależny od reprezentacji dekoder instrukcji. Ten dekoder odpowiada za przez wypełnianie pól obiektów instrukcji. Konkretne wartości, np. numer w rejestrze miejsc docelowych, należy uzyskać z instrukcji określonego formatu. za pomocą dekodera. Jednym z takich dekoderów jest dekoder binarny, w następnym samouczku.

Z tego samouczka dowiesz się, jak napisać plik z opisem ISA dla prostego, skalarnego i architekturą. Na podstawie zestawu instrukcji RiscV RV32I wraz z innymi samouczkami zbuduj symulator symulowania „Hello, World” programu. Więcej informacji o ISA RiscV ISA znajdziesz na stronie Specyfikacje Risc-V.

Otwórz plik: riscv_isa_decoder/riscv32i.isa

Zawartość pliku jest podzielona na wiele sekcji. Pierwszy to ISA. deklaracja:

isa RiscV32I {
  namespace mpact::sim::codelab;
  slots { riscv32; }
}

Spowoduje to deklarowanie, że RiscV32I będzie nazwą ISA, a generator kodu będzie i utwórz klasę o nazwie RiscV32IEncodingBase definiującą interfejs wygenerowany dekoder użyje do pobrania informacji o kodzie opcode i operandie. Nazwa tę klasę jest generowana przez konwertowanie nazwy ISA na format Pascal-case, a następnie łączenie jej za pomocą funkcji EncodingBase. Deklaracja slots { riscv32; } określa, że w RiscV32I jest tylko jeden boks instrukcji riscv32 ISA (w przeciwieństwie do wielu przedziałów w instrukcji VLIW) oraz że jedynym prawidłowe instrukcje to te zdefiniowane do wykonania w riscv32.

// First disasm fragment is 15 char wide and left justified.
disasm widths = {-15};

Określa to, że pierwszy fragment rozkładu dowolnego rozmontowanego elementu specyfikacja (więcej informacji poniżej), zostanie wyjustowana do 15 znaków szerokie pole. Wszystkie kolejne fragmenty będą dołączane do tego pola bez dodatkowych spacji.

Poniżej znajdują się 3 deklaracje przedziałów: riscv32i, zicsr i riscv32. Na podstawie powyższej definicji isa podano tylko instrukcje dla obiektu riscv32 boks będzie częścią tagu ISa RiscV32I. Do czego służą pozostałe 2 miejsca?

Przedziały mogą służyć do podziału instrukcji na osobne grupy, które można następnie połączone w jeden boks na końcu. Zwróć uwagę na zapis : riscv32i, zicsr w deklaracji przedziału riscv32. Określa, że boks riscv32 dziedziczy wszystkie instrukcje zdefiniowane w boksach zicsr i riscv32i. 32-bitowy ISA RiscV składa się z podstawowego komponentu ISA o nazwie RV32I, do którego mogą zostać dodane. Mechanizm przedziałów umożliwia przekazanie instrukcji w tych rozszerzeniach można określić oddzielnie, a następnie w razie potrzeby połączyć w celu zdefiniowania ISA. W tym przypadku instrukcje w dokumencie RiscV „I” grupa jest zdefiniowana niezależnie od tych zawartych w elemencie „zicsr” grupy reklam. Można zdefiniować dodatkowe grupy dla „M” (mnożenie/dziel), „F” (zmiennoprzecinkowa pojedynczej precyzji), „D” (zmiennoprzecinkowa podwójnej precyzji), „C” (kompaktowe 16-bitowe instrukcje) itp. potrzebne do uzyskania ostatecznej ostatecznej wersji RiscV ISA.

// The RiscV 'I' instructions.
slot riscv32i {
  ...
}

// RiscV32 CSR manipulation instructions.
slot zicsr {
  ...
}

// The final instruction set combines riscv32i and zicsr.
slot riscv32 : riscv32i, zicsr {
  ...
}

Definicji przedziałów zicsr i riscv32 nie musisz zmieniać. Jednak w tym samouczku będzie można dodać niezbędne definicje do riscv32i. gniazdo. Przyjrzyjmy się bliżej temu, co jest obecnie zdefiniowane w tym boksie:

// The RiscV 'I' instructions.
slot riscv32i {
  // Include file that contains the declarations of the semantic functions for
  // the 'I' instructions.
  includes {
    #include "learning/brain/research/mpact/sim/codelab/riscv_semantic_functions/solution/rv32i_instructions.h"
  }
  // These are all 32 bit instructions, so set default size to 4.
  default size = 4;
  // Model these with 0 latency to avoid buffering the result. Since RiscV
  // instructions have sequential semantics this is fine.
  default latency = 0;
  // The opcodes.
  opcodes {
    fence{: imm12 : },
      semfunc: "&RV32IFence"c
      disasm: "fence";
    ebreak{},
      semfunc: "&RV32IEbreak",
      disasm: "ebreak";
  }
}

Pierwsza z nich zawiera sekcję includes {} z listą plików nagłówka, które wymagają do uwzględnienia w wygenerowanym kodzie w momencie odwołania do tego boksu, bezpośrednio lub pośrednio w ostatecznej wersji ISA. Opcja „Uwzględnij pliki” może być również umieszczana w pliku globalnym includes {} o zakresie ograniczonym do zakresu. W takim przypadku są one zawsze uwzględniane. Może to spowodować jest przydatne, jeśli ten sam plik trzeba by dodać do każdego boksu. definicji.

Deklaracje default size i default latency określają, że, o ile jeśli nie określono inaczej, rozmiar instrukcji wynosi 4, a opóźnienie zapis operandu docelowego to 0 cykli. Zwróć uwagę, że rozmiar instrukcji jest tutaj rozmiarem przyrostu licznika programu do obliczenia adresu następnej instrukcji, która ma zostać wykonana w symulowanym procesora. Może, ale nie musi być taki sam, jak rozmiar w bajtach w wejściowym pliku wykonywalnym.

Centralnym punktem definicji przedziału jest sekcja kodu operacji. Jak widać, tylko dwa kody operacji (instrukcje) fence i ebreak zostały już zdefiniowane w riscv32i Kod operacji fence należy zdefiniować przez podanie nazwy (fence) i specyfikację operandu ({: imm12 : }) oraz opcjonalne polecenie demontażowe ("fence") oraz obiekt umożliwiający wywołanie, które zostanie powiązane jako semantyka funkcja ("&RV32IFence").

Argumenty instrukcji są określone jako potrójne, z których każdy oddzielone średnikiem, predykat „:” source operand list ':' lista operandów docelowych. Listy operandów źródłowych i docelowych są rozdzielone przecinkami oddzielone listy nazw operandów. Jak widać, operandy instrukcji dla argumentu instrukcja fence zawiera tylko jedno źródło, bez operandów predykatów nazwa operandu imm12 i brak operandów miejsca docelowego. Podzbiór RiscV RV32I spełnia nie obsługuje wykonywania predykatu, więc operand predykatu zawsze będzie pusty tego samouczka.

Funkcja semantyczna jest określona jako ciąg niezbędny do określenia języka C++. lub wykorzystywana do wywołania funkcji semantycznej. Podpis funkcja semantyczna/wywołana to void(Instruction *).

Specyfikacja rozkładu składa się z rozdzielanej przecinkami listy ciągów znaków. Zazwyczaj używane są tylko dwa ciągi: jeden dla kodu operacji, a drugi dla kodu operandy. Po sformatowaniu (za pomocą wywołania AsString() w instrukcji) każdy ciąg jest sformatowany w polu zgodnie z disasm widths specyfikacji omówionej powyżej.

Te ćwiczenia pomogą Ci dodać instrukcje do pliku riscv32i.isa wystarcza do zasymulowania „Hello, World” programu. Dla osób, które się spieszą, rozwiązania można znaleźć w riscv32i.isa oraz rv32i_instructions.h.


Wykonaj początkową kompilację

Jeśli katalog nie został zmieniony na riscv_isa_decoder, zrób to teraz. Potem utwórz projekt w następujący sposób – ta kompilacja powinna się udać.

$ cd riscv_isa_decoder
$ bazel build :all

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

W tym katalogu, między innymi plikami, znajdziesz: wygenerowane pliki w języku C++:

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

Kliknijmy pozycję riscv32i_enums.h w przeglądarce. Zalecenia zobaczysz, że zawiera on coś takiego:

#ifndef RISCV32I_ENUMS_H
#define RISCV32I_ENUMS_H

namespace mpact {
namespace sim {
namespace codelab {
  enum class SlotEnum {
    kNone = 0,
    kRiscv32,
  };

  enum class PredOpEnum {
    kNone = 0,
    kPastMaxValue = 1,
  };

  enum class SourceOpEnum {
    kNone = 0,
    kCsr = 1,
    kImm12 = 2,
    kRs1 = 3,
    kPastMaxValue = 4,
  };

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

  enum class OpcodeEnum {
    kNone = 0,
    kCsrs = 1,
    kCsrsNw = 2,
    kCsrwNr = 3,
    kEbreak = 4,
    kFence = 5,
    kPastMaxValue = 6
  };

  constexpr char kNoneName[] = "none";
  constexpr char kCsrsName[] = "Csrs";
  constexpr char kCsrsNwName[] = "CsrsNw";
  constexpr char kCsrwNrName[] = "CsrwNr";
  constexpr char kEbreakName[] = "Ebreak";
  constexpr char kFenceName[] = "Fence";
  extern const char *kOpcodeNames[static_cast<int>(
      OpcodeEnum::kPastMaxValue)];

  enum class SimpleResourceEnum {
    kNone = 0,
    kPastMaxValue = 1
  };

  enum class ComplexResourceEnum {
    kNone = 0,
    kPastMaxValue = 1
  };

  enum class AttributeEnum {
    kPastMaxValue = 0
  };

}  // namespace codelab
}  // namespace sim
}  // namespace mpact

#endif  // RISCV32I_ENUMS_H

Jak widać, każdy boks, kod opcode i operand zdefiniowane w Plik riscv32i.isa jest zdefiniowany w jednym z typów wyliczenia. Dodatkowo istnieje tablica OpcodeNames, w której są przechowywane wszystkie nazwy kodów operacji (to zdefiniowane w: riscv32i_enums.cc. Pozostałe pliki zawierają wygenerowany dekoder, Omówimy to dokładniej w kolejnym samouczku.

Reguła kompilacji w Bazelu

Cel dekodera ISA w Bazelu jest zdefiniowany za pomocą makra reguły niestandardowej o nazwie mpact_isa_decoder, wczytywany z mpact/sim/decoder/mpact_sim_isa.bzl w repozytorium mpact-sim. W tym samouczku cel kompilacji określony w riscv_isa_decoder/BUILD to:

mpact_isa_decoder(
    name = "riscv32i_isa",
    src = "riscv32i.isa",
    includes = [],
    isa_name = "RiscV32I",
    deps = [
        "//riscv_semantic_functions:riscv32i",
    ],
)

Ta reguła wywołuje narzędzie parsera i generator ISA do wygenerowania kodu C++, a potem kompiluje wygenerowane dane w bibliotekę, na której mogą uzależnić się inne reguły etykietę //riscv_isa_decoder:riscv32i_isa. Używana jest sekcja includes aby określić dodatkowe pliki (.isa), które może zawierać plik źródłowy. isa_name służy do określenia, która konkretnie wartość ISA jest wymagana, jeśli podano więcej niż 1 w pliku źródłowym, dla którego ma zostać wygenerowany dekoder.


Dodaj instrukcje dotyczące rejestracji i rejestracji konta ALU

Teraz musisz dodać nowe instrukcje do pliku riscv32i.isa. Pierwszy instrukcje dotyczące rejestracji ALU, takie jak add, and itd. W RiscV32 wszystkie używają formatu instrukcji binarnych typu R:

31..25 24..20 19.01.2015 14..12 11..7 6..0
7 5 5 3 5 7
func7 rs2 rs1 func3 rd kod operacji

Plik .isa służy do wygenerowania dekodera niezależnego od formatu, ale nadal jest warto wziąć pod uwagę format binarny i jego układ, by kierować wpisami. Gdy widać trzy pola, które są istotne dla dekodera wypełniającego pole obiekty instrukcji: rs2, rs1 i rd. Teraz wybierzemy opcję te nazwy rejestrów całkowitych, które są zakodowane w ten sam sposób (sekwencje bitów), w tych samych polach we wszystkich instrukcjach.

Instrukcje, które dodamy, są następujące:

  • add – dodanie liczby całkowitej.
  • and – bitowe i.
  • or – bitowa lub
  • sll – przesunięcie w lewo – wartość logiczna.
  • sltu – ustaw wartość „mniejsze niż”, „bez podpisu”.
  • sub – odejmowanie liczby całkowitej.
  • xor – bitowa xor.

Każda z tych instrukcji zostanie dodana do sekcji opcodes w dokumencie Definicja przedziału: riscv32i. Musimy podać nazwę, kod opcode funkcji desemantycznej i semantycznej dla każdej instrukcji. Nazwa jest prosta, użyjmy podanych wyżej nazw kodów opcode. Poza tym wszystkie używają tych samych operandów, więc możemy użyć { : rs1, rs2 : rd} jako specyfikacji operandu. Oznacza to, że operand źródłowy rejestru określony przez rs1 będzie miał w źródle indeks 0 wektor operandu w obiekcie instrukcji, określono operand źródłowy rejestru przez rs2 będzie mieć indeks 1, a operand rejestracji miejsca docelowego określony przez rd będzie jedynym elementem w docelowym wektorze operandu (w indeksie 0).

Kolejnym krokiem jest specyfikacja funkcji semantycznej. Służą do tego słowa kluczowe semfunc i ciąg znaków w C++ określający obiekt umożliwiający wywołanie, którego można użyć do przypisania do: std::function. W tym samouczku użyjemy funkcji, dzięki czemu funkcja wywoływania będzie mieć wartość "&MyFunctionName". Stosując schemat nazewnictwa sugerowanego przez instrukcja fence, powinny to być "&RV32IAdd", "&RV32IAnd" itd.

Na końcu mamy specyfikację dotyczącą rozkładu. Zaczyna się od słowa kluczowego disasm znajduje się lista ciągów znaków oddzielonych przecinkami, która określa sposób powinna być wydrukowana jako ciąg znaków. Użyj znaku % przed tagiem nazwa operandu wskazuje zastępowanie ciągu znaków za pomocą reprezentacji ciągu ten operand. Dla instrukcji add będzie to: disasm: "add", "%rd, %rs1,%rs2". Oznacza to, że wpis instrukcji add powinien wyglądać tak lubię:

    add{ : rs1, rs2 : rd},
      semfunc: "&RV32IAdd",
      disasm: "add", "%rd, %rs1, %rs2";

Edytuj plik riscv32i.isa i dodaj wszystkie te instrukcje do .isa tekst reklamy. Jeśli potrzebujesz pomocy (lub chcesz sprawdzić swoje zadanie), pełne plik z opisem to tutaj.

Gdy dodasz instrukcje do pliku riscv32i.isa, dodać deklaracje funkcji dla każdej z nowych funkcji semantycznych, odwołuje się do pliku rv32i_instructions.h znajdującego się w `../semantic_functions/. Jeśli potrzebujesz pomocy (lub chcesz sprawdzić swoją pracę), odpowiedź to tutaj.

Gdy to zrobisz, przejdź z powrotem na riscv_isa_decoder i przebuduj go. Możesz przejrzeć wygenerowane pliki źródłowe.


Dodawanie instrukcji dotyczących ALU z natychmiastowymi poleceniami natychmiastowymi

Następny zestaw instrukcji to instrukcje dotyczące ALU, które wykorzystują natychmiastową wartość zamiast jednego z rejestrów. Wyróżniamy 3 grupy instrukcje (na podstawie bezpośredniego pola): instrukcje I-Type natychmiastowe z 12-bitowym podpisem natychmiastowym, specjalistycznymi natychmiastowymi instrukcjami I-Type dla przesunięć oraz natychmiastowy typ U z 20-bitową wartością natychmiastową bez znaku. Formaty są wymienione poniżej:

Bezpośredni format typu I:

31.20 19.01.2015 14..12 11..7 6..0
12 5 3 5 7
imm12 rs1 func3 rd kod operacji

Specjalny format bezpośredni typu I-Type:

31..25 24..20 19.01.2015 14..12 11..7 6..0
7 5 5 3 5 7
func7 uimm5 rs1 func3 rd kod operacji

Natychmiastowy format U-Type:

31..12 11..7 6..0
20 5 7
uimm20 rd kod operacji

Jak widać, nazwy operandów rs1 i rd odnoszą się do tych samych pól bitowych co i służą do reprezentowania rejestrów całkowitych, więc nazwy te można i przechowywania danych. Pola wartości bezpośrednich mają różną długość i lokalizację oraz dwa (uimm5 i uimm20) są nieoznaczone, a imm12 jest podpisane. Każda z tych wartości: będą używać własnych nazw.

operandy instrukcji I-Type powinny więc mieć postać { : rs1, imm12 :rd }. W przypadku specjalistycznych instrukcji I-Type powinna to być wartość { : rs1, uimm5 : rd}. Specyfikacja operandu instrukcji typu U powinna mieć postać { : uimm20 : rd }.

Instrukcje, które musimy dodać, są następujące:

  • addi – dodaj natychmiastowe.
  • andi – bitowa i natychmiastowa.
  • ori – bitowa lub natychmiastowa.
  • xori – bitowe xor z trybem natychmiastowym.

Specjalistyczne instrukcje I-Type, które musimy dodać:

  • slli – przesunięcie w lewo o wartość logiczną natychmiastowe.
  • srai – przesunięcie arytmetyczne w prawo według trybu natychmiastowego.
  • srli – przesunięcie logiczne w prawo o tryb natychmiastowy.

Instrukcje typu U, które musimy dodać:

  • auipc – dodaj natychmiastowe górne dane do komputera.
  • lui – wczytaj od razu u góry.

Nazwy używane dla kodów operacji naturalnie wynikają z nazw instrukcji powyżej (nie trzeba wymyślać nowych – są niepowtarzalne). Jeśli chodzi o określając funkcje semantyczne, pamiętaj, że obiekty instrukcji kodują interfejsy do operandów źródłowych, które są niezależne od bazowego operandu typu. Oznacza to, że w przypadku instrukcji wykonujących tę samą operację, ale może różnić się typami operandów, może korzystać z tej samej funkcji semantycznej. Przykład: instrukcja addi wykonuje takie samo działanie jak instrukcja add, jeśli powoduje zignorowanie typu operandu, więc można w nim używać tej samej funkcji semantycznej specyfikacja "&RV32IAdd". Podobnie w przypadku elementów: andi, ori, xori i slli. W pozostałych instrukcjach są używane nowe funkcje semantyczne, ale należy je nazwać. na podstawie operacji, a nie operandów, więc w polu srai użyj "&RV32ISra". Instrukcje typu U auipc i lui nie mają odpowiednika w rejestrze, więc są prawidłowe aby użyć "&RV32IAuipc" i "&RV32ILui".

Ciągi rozkładu są bardzo podobne do tych w poprzednim ćwiczeniu, ale zgodnie z oczekiwaniami odwołania do %rs2 zostały zastąpione przez %imm12, %uimm5, lub %uimm20.

Wprowadź zmiany i zacznij budować. Sprawdź wygenerowane dane wyjściowe. Tak samo możesz sprawdzić swoją pracę pod kątem riscv32i.isa oraz rv32i_instructions.h.


Instrukcje dotyczące gałęzi i linków, które musimy dodać, muszą być przypisane do miejsca docelowego operand, który jest dorozumiany tylko w instrukcji, a mianowicie następny pc . Na tym etapie potraktujemy to jako właściwy operand z nazwą next_pc Szczegółowo wyjaśnimy to w dalszej części samouczka.

Instrukcje dotyczące gałęzi

Gałęzie, które dodajemy, korzystają z kodowania typu B.

31 30..25 24..20 19.01.2015 14..12 11..8 7 6..0
1 6 5 5 3 4 1 7
Imm Imm rs2 rs1 func3 Imm Imm kod operacji

Różne pola bezpośrednie są połączone w 12-bitowy znak bezpośredni . Ponieważ format nie jest właściwy, zajmę się tym natychmiastowym, bimm12, dla gałęzi 12-bitowej natychmiast. Fragmentacja zostanie omówiona w zobaczysz następny samouczek tworzenia dekodera plików binarnych. Wszystkie instrukcje rozgałęzienia porównują rejestry całkowite określone przez rs1 i rs2, jeśli jeśli warunek jest prawdziwy, natychmiastowa wartość jest dodawana do bieżącej wartości PCC uzyskać adres następnej instrukcji do wykonania. operandy funkcji instrukcje oddziału powinny więc mieć postać { : rs1, rs2, bimm12 : next_pc }.

Instrukcje dotyczące gałęzi, które musimy dodać, są następujące:

  • beq – gałąź, jeśli jest równa.
  • bge – gałąź, jeśli jest większa lub równa.
  • bgeu – gałąź, jeśli jest większa lub równa bez znaku.
  • blt – gałąź, jeśli ma wartość mniejszą niż.
  • bltu – gałąź, jeśli jest mniejsza niż brak podpisu.
  • bne – gałąź, jeśli nie jest równa.

Te nazwy kodów operacji są unikalne, więc można ich ponownie użyć w funkcji .isa opis. Oczywiście musisz dodać nowe nazwy funkcji semantycznych, np. "&RV32IBeq" itp.

Specyfikacja rozkładu jest teraz nieco trudniejsza, ponieważ adres jest wykorzystywana do obliczenia miejsca docelowego, a nie jest operandów instrukcji. Stanowią one jednak część informacji przechowywanych obiekt instrukcji, aby był dostępny. Rozwiązaniem jest użycie w ciągu znaków do dezasemblowania. Zamiast „%” , a następnie nazwę argumentu, możesz wpisać %(wyrażenie: format wydruku). Tylko bardzo proste wyrażenia są obsługiwane, ale wśród nich jest adres z przesunięciem adresu, z @ używany dla aktualnego adresu instrukcji. Format wydruku jest podobny do Formaty wydrukuf w stylu C, ale bez początkowego %. Format zdemontowania dla instrukcja beq stanie się wtedy:

    disasm: "beq", "%rs1, %rs2, %(@+bimm12:08x)"

Należy dodać tylko 2 instrukcje szybkiego rozpoczynania linku: jal (przewijanie i link) oraz jalr (pośrednie przejście i link).

Instrukcja jal korzysta z kodowania J-Type:

31 30..21 20 19.012 11..7 6..0
1 10 1 8 5 7
Imm Imm Imm Imm rd kod operacji

Podobnie jak w przypadku instrukcji rozgałęzionych, 20-bitowy plik bezpośredni jest podzielony między jest kilka pól, więc nadajemy mu nazwę jimm20. Fragmentacja nie ma znaczenia obecnie zajmujemy się tym, ale zajmiemy się tym jak utworzyć dekoder plików binarnych. Argument specyfikacji zmieni się na { : jimm20 : next_pc, rd }. Pamiętaj, że musisz wiedzieć, że operandy miejsca docelowego, następna wartość pc i rejestr linku określony w wraz z instrukcjami.

Podobnie jak w przypadku powyższych instrukcji dotyczących rozgałęzienia, format rozkładu wygląda tak:

    disasm: "jal", "%rd, %(@+jimm20:08x)"

Pośrednie przejście typu „już i link” wykorzystuje format I-Type z 12-bitowym systemem natychmiastowym. it dodaje natychmiastową wartość o rozszerzeniach ze znakiem do rejestru całkowitej wyznaczonego przez rs1 w celu utworzenia docelowego adresu instrukcji. Rejestr linków to rejestr całkowity określony przez rd.

31.20 19.01.2015 14..12 11..7 6..0
12 5 3 5 7
imm12 rs1 func3 rd kod operacji

Jeśli znasz ten wzorzec, można wywnioskować, że specyfikacja operandu dla jalr powinien mieć wartość { : rs1, imm12 : next_pc, rd }, a demontaż specyfikacja:

    disasm: "jalr", "%rd, %rs1, %imm12"

Wprowadź zmiany i zacznij budować. Sprawdź wygenerowane dane wyjściowe. Tylko tak jak poprzednio, możesz porównać swoją pracę z riscv32i.isa oraz rv32i_instructions.h.


Dodaj instrukcje dotyczące sklepu

Instrukcje dla sklepu są bardzo proste. Wszystkie mają format S:

31..25 24..20 19.01.2015 14..12 11..7 6..0
7 5 5 3 5 7
Imm rs2 rs1 func3 Imm kod operacji

Jest to kolejny przypadek fragmentarycznego 12-bitowego formatu natychmiastowego nadaj nazwę simm12. Instrukcje dotyczące sklepu zapisują wartość liczby całkowitej rejestr określony przez rs2 do efektywnego adresu w pamięci uzyskanego przez dodanie wartość rejestru całkowitej określonej przez funkcję rs1 do wartości rozszerzonego znaku lub 12-bitowy. Argument powinien być w formacie { : rs1, simm12, rs2 } wszystkie instrukcje dotyczące sklepu.

Instrukcje dotyczące sklepu, które należy wdrożyć:

  • sb – zapisany bajt.
  • sh – zapisz pół słowa.
  • sw – słowo zapisane w sklepie.

Specyfikacja demontażu elementu sb jest zgodna z oczekiwaniami:

    disasm: "sb", "%rs2, %simm12(%rs1)"

Specyfikacje funkcji semantycznych też są zgodne z oczekiwaniami: "&RV32ISb", ip.

Wprowadź zmiany i zacznij budować. Sprawdź wygenerowane dane wyjściowe. Tylko tak jak poprzednio, możesz porównać swoją pracę z riscv32i.isa oraz rv32i_instructions.h.


Dodaj instrukcje wczytywania

Instrukcje wczytywania są modelowane nieco inaczej niż pozostałe w symulatorze. Aby można było modelować przypadki, w których opóźnienie wczytywania jest niepewne, instrukcje wczytywania są podzielone na dwa osobne działania: 1) skuteczne adresu obliczeniowego i dostępu do pamięci oraz 2) zapis zwrotny wyników. W w symulatorze, dzieląc działanie semantyczne obciążenia na dwie osobne instrukcje, instrukcję główną i instrukcję podrzędną. Ponadto, gdy określamy operandy, musimy je podać zarówno dla child. W tym celu specyfikacja operandu jest traktowana jako triole. Składnia:

{(predicate : sources : destinations), (predicate : sources : destinations), ... }

We wszystkich instrukcjach ładowania jest używany format I-Type, tak samo jak wiele poprzednich instrukcje:

31.20 19.01.2015 14..12 11..7 6..0
12 5 3 5 7
imm12 rs1 func3 rd kod operacji

Specyfikacja operandu dzieli operandy niezbędne do obliczenia adresu i zainicjuj dostęp do pamięci z miejsca docelowego rejestru dla danych wczytywania: {( : rs1, imm12 : ), ( : : rd) }

Działanie semantyczne dzieli się na dwie instrukcje, dlatego funkcje semantyczne analogicznie, trzeba określić dwa obiekty wywołujące. W przypadku słowa kluczowego lw (załaduj słowo) będzie to wartość pisemne:

    semfunc: "&RV32ILw", "&RV32ILwChild"

Specyfikacja dotycząca rozkładu jest bardziej konwencjonalna. Nie pojawia się wzmianka o wychowanie dziecka. W przypadku lw powinno to być:

    disasm: "lw", "%rd, %imm12(%rs1)"

Instrukcje wczytywania, które musisz zaimplementować:

  • lb – wczytano bajt.
  • lbu – wczytanie bajtów bez podpisu.
  • lh – wczytaj półwyrazu.
  • lhu – wczytaj półwyraz bez podpisu.
  • lw – wczytaj słowo.

Wprowadź zmiany i zacznij budować. Sprawdź wygenerowane dane wyjściowe. Tylko tak jak poprzednio, możesz porównać swoją pracę z riscv32i.isa oraz rv32i_instructions.h.

Dziękuję, że zajdziesz tak daleko. Mamy nadzieję, że te informacje okażą się przydatne.