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:
- Adres instrukcji, symulowany rozmiar instrukcji, czyli rozmiar w formacie .text.
- Kod operacji instrukcji.
- Wskaźnik interfejsu predykatu (jeśli występuje).
- Wektor wskaźników interfejsu operandu źródłowego.
- Wektor wskaźników interfejsu operandu docelowego.
- Funkcja semantyczna może być wywoływana.
- Wskaźnik do obiektu stanu architektury.
- Wskaźnik do obiektu kontekstu.
- Wskaźnik do podrzędnych i kolejnych instancji instrukcji.
- 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 lubsll
– 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.
Dodawanie instrukcji dotyczących gałęzi i linku typu „jump and-link”
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)"
Instrukcje dotyczące przechodzenia i linków
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.