RiscV ISA 解碼器

本教學課程的目標是:

  • 瞭解 MPACT-Sim 模擬器如何表示指示。
  • 瞭解 ISA 說明檔案的結構和語法。
  • 為 RiscV RV32I 部分指示撰寫 ISA 說明

總覽

在 MPACT-Sim 中,目標指令會解碼並儲存在內部表示法中,使其資訊更易於取得,且語意加快執行速度。這些指令例項會在指令快取中快取,以減少執行頻繁的指令執行次數。

教學課程

在開始之前,先來看看 MPACT-Sim 如何呈現指令。Instruction 類別是在 mpact-sim/mpact/sim/generic/instruction.h 中定義。

指令類別例項包含模擬指令在「執行」時所需的所有資訊,例如:

  1. 指令位址、模擬指令大小,也就是 .text 中的大小。
  2. 命令 Ocode。
  3. 述詞運算元介面指標 (如適用)。
  4. 來源運算元介面指標的向量。
  5. 目標運算元介面指標的向量。
  6. 可呼叫的語意函式。
  7. 架構狀態物件的指標。
  8. 指向內容物件的指標。
  9. 指向子項和下一個指令例項的指標。
  10. 反組字串。

這些例項通常會儲存在指令 (例項) 快取中,並在重新執行指令時重複使用。這麼做可改善執行階段期間的效能。

除了指向內容物件的指標外,所有項目都會由從 ISA 說明產生的指令解碼器填入。在本教學課程中,您不必瞭解這些項目的詳細資料,因為我們不會直接使用這些項目。只要大致瞭解它們的用途即可。

語意函式可呼叫為實作指示語意的 C++ 函式/方法/函式物件 (包括 lambda)。舉例來說,針對 add 指令,它會載入每個來源運算元,將兩個運算元加總,然後將結果寫入單一目的地運算元。在語意函式教學課程中,我們會深入探討語意函式主題。

指令運算元

指令類別包含三種運算元介面的指標:述詞、來源和目的地。這些介面可讓語意函式獨立於基礎指令運算元的實際類型編寫。舉例來說,如要存取暫存器和立即事件的值,請透過相同的介面完成。這表示針對不同運算元 (例如暫存器與動畫) 執行相同作業的指示,也可以使用相同的語意函式實作。

述詞運算元介面 (針對支援詳細指示執行的 ISAs (針對其他 ISA 為空值) 的述詞運算元介面,用於判斷是否應根據述詞的布林值執行特定指令。

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

來源運算元介面允許指示語意函式從指令運算元讀取值,而無需考慮基礎運算元類型。介面方法可同時支援純量和向量值運算元。

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

目的地運算元介面提供方法,用於分配及處理 DataBuffer 例項 (用於儲存登錄值的內部資料類型)。目的運算元也具有相關的延遲時間,也就是等待使用指令語義函式所配置的資料緩衝區例項,用於更新目標登錄值的週期數。舉例來說,add 指令的延遲時間可能是 1,而 mpy 指示的延遲時間則可能是 4。如需進一步瞭解這項功能,請參閱語意函式的教學課程。

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

ISA 說明

處理器的 ISA (指令集架構) 會定義軟體與硬體互動的抽象模型。定義了一組可用操作說明、資料類型、暫存器和其他操作說明用來運作的機器狀態,以及其行為 (語意)。為了 MPACT-Sim 的目的,ISA 不包含指令的實際編碼。這部分會單獨處理。

處理器 ISA 會以說明檔案表示,當中說明瞭在各抽象、跨編碼層級設定的指令。說明檔案會列舉可用的指令集。每項指令都必須列出名稱、運算元的數量和名稱,以及其繫結至實作語意的 C++ 函式/呼叫項目。此外,您可以指定反組裝格式字串,以及指令對硬體資源名稱的用法。前者有助於產生文字表示法,方便偵錯、追蹤或互動用途。後者可用於在模擬中建構更多週期準確率。

ISA 說明檔案會由 isa-parser 剖析,後者會為不區分表示法的指令解碼器產生程式碼。這個解碼器會負責填入指示物件的欄位特定值 (例如目的地註冊編號) 是從格式專屬指令解碼器取得。其中一個解碼器是二進位檔解碼器,這是下一堂教學課程的重點。

本教學課程將說明如何為簡單的單一架構編寫 ISA 說明檔案。我們將使用 RiscV RV32I 指令集的子集來說明這一點,並搭配其他教學課程,建構可模擬「Hello World」程式的模擬器。如要進一步瞭解 RiscV ISA,請參閱「Risc-V 規格」。

首先,開啟檔案: riscv_isa_decoder/riscv32i.isa

檔案內容分成多個部分。首先是宣告式匯入檔案:

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

這會將 RiscV32I 宣告為 ISA 名稱,程式碼產生器會建立名為 RiscV32IEncodingBase 的類別,該類別會定義產生的解碼器將用於取得運算碼和運算元資訊的介面。這個類別的名稱是將 ISA 名稱轉換成 Pascal 命名法,然後再與 EncodingBase 串連,以產生這個類別的名稱。宣告 slots { riscv32; } 表示 RiscV32I ISA 只有一個指令版位 riscv32 (而不是 VLIW 指令中的多個版位),而且唯一有效的指示是在 riscv32 中定義的執行。

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

這表示任何反散文規範的第一個拆解片段 (詳情請參閱下方) 將在全字元寬的欄位中保留。後續的任何片段都會附加到這個欄位,且不會有任何額外的空格。

這個下方有三個版位宣告:riscv32izicsrriscv32。根據上述的 isa 定義,只有針對 riscv32 位置定義的指示將是 RiscV32I 為的一部分。另外兩個插槽是用來做什麼?

您可以使用空格將指令分解為個別群組,然後在最後將這些群組合併為單一空格。請注意 riscv32 插槽宣告中的符號 : riscv32i, zicsr。這會指定運算單元 riscv32 沿用版位 zicsrriscv32i 中定義的所有指示。RiscV 32 位元 ISA 包含稱為 RV32I 的基準 ISA,可加入一組選用的擴充功能。插槽機制可讓您分別指定這些擴充功能中的指令,然後視需要合併,以定義整體 ISA。在此例中,RscV「I」群組中的指示與「zicsr」群組中的指示分開定義。您可以視需要為最終的 RiscV ISA 定義「M」(乘法/除數)、「F」(單精度浮點)、「D」(雙精度浮點)、「C」(精簡 16 位元指示) 等。

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

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

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

您不需要變更 zicsrriscv32 的版位定義。不過,本教學課程的重點是將必要定義新增至 riscv32i 位置。讓我們進一步瞭解這個版位目前的定義:

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

首先,有一個 includes {} 區段,列出在最終 ISA 中直接或間接參照這個位置時,所需納入產生程式碼中的標頭檔案。您也可以將檔案納入全域範圍的 includes {} 區段,這樣檔案就會一律納入其中。如果同一個 include 檔案必須加入至每個版位定義,這項功能就會很實用。

default sizedefault latency 宣告會定義,除非另有指定,否則指令大小為 4,且目的地運算元寫入的延遲時間為 0 個週期。請注意,此處指定的指令大小是指程式計數器遞增大小,用於計算在模擬處理器中要執行的下一個順序指令的位址。這個值不一定與輸入執行檔中指令表示法的大小 (以位元組為單位)。

執行碼區是 Slot 定義的核心。如您所見,目前 riscv32i 中只定義了兩個運算碼 (操作說明) fenceebreak。定義 fence 運算碼的方式是指定名稱 (fence) 和運算元規格 ({: imm12 : }),後面接著選用的反組譯格式格式 ("fence"),以及要繫結為語意函式 ("&RV32IFence") 的呼叫。

指令運算元會指定為三元組,每個元件都以半形分號 (predicate ':' source operand list ':' destination operand list) 指定。來源和目的地運算元項清單是運算元項名稱的清單,以半形逗號分隔。如您所見,fence 指令的運算子不含判定式運算子,只有單一來源運算子名稱 imm12,也沒有目的地運算子。RiscV RV32I 子集不支援預測執行作業,因此在本教學課程中,預測運算元將一律為空白。

語意函式會指定為字串,用於指定 C++ 函式或可呼叫的函式,以便用來呼叫語意函式。語意函式/可呼叫的簽名為 void(Instruction *)

反組譯規格由以半形逗號分隔的字串清單組成。通常只會使用兩個字串,一個用於 Opcode,另一個用於運算元。格式化時 (使用指令中的 AsString() 呼叫),每個字串都會根據上述 disasm widths 規格在欄位中進行格式化。

以下練習可協助您在 riscv32i.isa 檔案中新增足以模擬「Hello World」程式的指示。如要快速解決問題,請參閱 riscv32i.isarv32i_instructions.h


執行初始建構

如果您尚未將目錄變更為 riscv_isa_decoder,請立即進行。然後如下建構專案,這項建構作業應會成功。

$ cd riscv_isa_decoder
$ bazel build :all

現在請將目錄變更回存放區根目錄,然後查看產生的來源。為此,請將目錄變更為 bazel-out/k8-fastbuild/bin/riscv_isa_decoder (假設您使用的是 x86 主機 - 對於其他主機,k8-fastbuild 會是另一個字串)。

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

這個目錄中的其他檔案中,會有下列產生的 C++ 檔案:

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

首先,請在瀏覽器中按一下riscv32i_enums.h。您應該會看到內含類似下方的內容:

#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

如您所見,在 riscv32i.isa 檔案中定義的每個槽、運算碼和運算元皆在其中一個列舉型別中定義。此外,還有一個 OpcodeNames 陣列,可儲存所有 Opcode 的名稱 (在 riscv32i_enums.cc 中定義)。其他檔案包含產生的解碼器,我們會在其他教學課程中進一步介紹。

Bazel 建構規則

Bazel 中的 ISA 解碼器目標是使用名為 mpact_isa_decoder 的自訂規則巨集定義,該巨集會從 mpact-sim 存放區的 mpact/sim/decoder/mpact_sim_isa.bzl 載入。在本教學課程中,riscv_isa_decoder/BUILD 中定義的建構目標為:

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

這個規則會呼叫 ISA 剖析器工具和產生器,產生 C++ 程式碼,然後將產生的程式碼編譯為其他規則可依附的程式庫,使用標籤 //riscv_isa_decoder:riscv32i_isaincludes 區段可用來指定來源檔案可能包含的其他 .isa 檔案。指定多個特定 Ia (如果指定多個) 時,會在要產生解碼器的來源檔案中使用 isa_name


新增 Register-Register ALU 指令

接下來,請在 riscv32i.isa 檔案中新增一些指示。第一個操作說明群組是註冊 ALU 指令,例如 addand 等。在 RiscV32 上,這些指令都會使用 R 型二進位檔指令格式:

31..25 24..20 19..15 14 月 12 日 11..7 6..0 版
7 5 5 3 5 7
func7 rs2 rs1 func3 rd 運算程式碼

雖然 .isa 檔案可用於產生格式無關的解碼器,但考量到二進位格式及其版面配置,仍可用於引導項目。如您所見,有三個欄位與填入指令物件的解碼器相關:rs2rs1rd。目前,我們會選擇將這些名稱用於在相同的指示欄位中,以相同方式 (位元序列) 編碼的整數暫存器。

我們要新增的操作說明如下:

  • add - 加入整數,
  • and - 位元 AND。
  • or - 位元或。
  • sll - 邏輯左移。
  • sltu - 設定小於或未簽署的字元。
  • sub - 整數減法。
  • xor - 位元 XOR。

每個指令都會新增至 riscv32i 版位定義的 opcodes 區段。請注意,我們必須為每個指令指定名稱、opcode、反組譯和語意函式。名稱很簡單 僅使用上方的運算程式碼名稱此外,它們都使用相同的運算元,因此我們可以使用 { : rs1, rs2 : rd} 做為運算元規格。這表示 rs1 指定的註冊器來源運算元在指令物件的來源運算元向量中會有索引 0,rs2 指定的註冊器來源運算元會有索引 1,而 rd 指定的註冊器目的地運算元則是目的地運算元向量中唯一的元素 (索引 0)。

接下來是語意函式規格。這項操作會使用關鍵字 semfunc 和 C++ 字串,指定可用來指派至 std::function 的可呼叫項目。在本教學課程中,我們會使用函式,因此可呼叫的字串會是 "&MyFunctionName"。使用 fence 指令建議的命名配置,應為 "&RV32IAdd""&RV32IAnd" 等。

最後是拆解規格開頭為 disasm 關鍵字,後方以半形逗號分隔的字串清單,用於指定如何將指令以字串形式列印。在運算元名稱前面使用 % 符號,表示使用該運算元的字串表示法進行字串取代。如果是 add 指令,則應為:disasm: "add", "%rd, %rs1,%rs2"。這表示 add 指令的項目應如下所示:

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

請繼續編輯 riscv32i.isa 檔案,並將所有這些操作說明加入 .isa 說明中。如需協助 (或想查看自己的工作成果),請參閱這裡的完整說明檔案。

riscv32i.isa 檔案中新增指令後,您必須針對每個參照到 rv32i_instructions.h`../semantic_functions/ 檔案中的新語意函式,新增函式宣告。再次提醒,如需協助 (或想檢查自己的工作),請參閱這篇文章

完成上述操作後,請返回 riscv_isa_decoder 目錄並重新建構。您可以檢查產生的來源檔案。


新增 ALU 指示與立即通知

接下來,我們將新增一組 ALU 指令,使用立即值而非其中一個暫存器。這些操作說明分成三個群組 (根據立即欄位):I-Type 立即提供 12 位元立即簽署的指示、特殊 I-Type 即時指示,以及 U-Type,以及 20 位元未簽署立即值。格式如下:

I 型立即格式:

2020 年 3 月 31 日 19..15 14 月 12 日 11..7 6..0 版
12 5 3 5 7
imm12 rs1 func3 rd 運算程式碼

專用的 I-Type 立即格式:

2025 年 31 月 31 日 24..20 19..15 14 月 12 日 11..7 6..0 版
7 5 5 3 5 7
func7 uimm5 rs1 func3 rd 運算程式碼

U 型立即格式:

31..12 11..7 6..0
20 5 7
uimm20 rd 運算程式碼

如您所見,運算元名稱 rs1rd 參照與先前相同的位元欄位,並用於代表整數暫存器,因此這些名稱可以保留。即時值欄位的長度和位置不同,其中兩個 (uimm5uimm20) 為未簽署狀態,而 imm12 則是帶正負號。每個選項都會有自己的名稱。

因此,I-Type 指令的運算元應為 { : rs1, imm12 :rd }。如需特殊的 I-Type 指示,則應為 { : rs1, uimm5 : rd}。U 型指令運算元規格應為 { : uimm20 : rd }

需要新增的 I-Type 操作說明如下:

  • addi - 立即新增。
  • andi - 位元和立即。
  • ori - 位元或運算與立即值。
  • xori - 具有立即的位元 xor。

需要新增的特殊 I-Type 指示如下:

  • slli - 立即將左移邏輯列。
  • srai - 立即將右算法調整。
  • srli - 立即將右移邏輯運算。

我們需要新增的 U 型指令如下:

  • auipc - 立即將上限新增至電腦。
  • lui - 立即載入上層。

用於 Opcode 的名稱會自然地從上述指令名稱開始 (不需要提出新的名稱,因為這些名稱都是獨特的)。在指定語意函式時,請回想一下,指令物件會將介面編碼至來源運算元,而這些介面不受基礎運算元類型的影響。換句話說,如果指令具有相同的運算,但運算元式類型可能不同,則可以共用相同的語意函式。舉例來說,如果忽略運算元項類型,addi 指令會執行與 add 指令相同的作業,因此可以使用相同的語意函式規格 "&RV32IAdd"andiorixorislli 也是一樣。其他指令會使用新的語意函式,但應根據運算而非運算元件命名,因此請針對 srai 使用 "&RV32ISra"。U 型指令 auipclui 沒有等同的註冊,因此可以使用 "&RV32IAuipc""&RV32ILui"

反組譯字串與先前練習中的方法非常相似,但您可以預期將 %rs2 的參照替換為 %imm12%uimm5%uimm20 (可視情況而定)。

請繼續進行變更並建構。查看產生的輸出內容。和先前一樣,您可以檢查 riscv32i.isarv32i_instructions.h 中的工作。


我們需要新增的分支和跳躍連結指令都會使用目的運算子,而這個運算子只會在指令本身中暗示,也就是下一個 pc 值。在這個階段,我們會將這個值視為名為 next_pc 的適當運算元。我們會在後續教學課程中進一步定義。

分支版本操作說明

我們新增的分支全都使用 B-Type 編碼。

31 30..25 24..20 19..15 14..12 11..8 7 6..0 版
1 6 5 5 3 4 1 7
imm imm rs2 rs1 func3 imm imm 運算程式碼

不同的立即欄位會串連為 12 位元的立即簽署值。由於格式不相關,我們會立即稱為 bimm12,適用於 12 位元分支版本。我們會在下一個教學課程中說明如何建立二進位檔解碼器。所有分支指令都會比較 rs1 和 rs2 指定的整數登錄,如果條件為真,立即值會加到目前的 pc 值,產生要執行的下一個指令位址。因此,分支版本指令的運算元應為 { : rs1, rs2, bimm12 : next_pc }

我們需要新增的分支指令如下:

  • beq - 若相等則分支。
  • bge:如果大於或等於,則分支。
  • bgeu - 大於或等於未簽署的分支版本。
  • blt:如果小於,則分支。
  • bltu:如果小於未簽署值,則分支。
  • bne - 否則為分支版本。

這些操作碼名稱皆不重複,因此可在 .isa 說明中重複使用。當然,必須新增語意函式名稱,例如"&RV32IBeq"

由於指令的位址會用於計算目的地,但實際上並非指令運算元的一部分,因此現在的反組規格會稍微複雜一些。但這是操作說明物件中儲存的資訊的一部分,因此可供存取。解決方法是在反組譯字串中使用運算式語法。您可以輸入 %(運算式: 列印格式),而非使用「%」後接運算子名稱。系統只支援非常簡單的運算式,但地址加上偏移量是其中一個運算式,其中包含用於目前指令位址的 @ 符號。列印格式與 C 樣式列印格式類似,但沒有開頭的 %beq 指令的反組裝格式就會變成:

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

您只需要新增兩個跳轉與連結操作說明,即 jal (跳至跳轉連結) 和 jalr (間接跳連結)。

jal 指令使用 J-Type 編碼:

31 2021 年 3 月 30 日 20 19:12 11..7 6..0
1 10 1 8 5 7
imm imm imm imm 運算程式碼

如同分支版本操作說明,20 位元立即會在多個欄位中分段,因此我們將將其命名為 jimm20。片段現在並不重要,但我們會在下一個教學課程中探討如何建立二進位檔解碼器。運算元規格會成為 { : jimm20 : next_pc, rd }。請注意,有兩個目的地運算元,分別是指示中的下一個 PC 值和連結暫存器。

與上述分支指令類似,解組格式會變成:

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

間接跳躍和連結會使用 I 型格式,並搭配 12 位元立即值。它會將符號擴展立即值加到 rs1 指定的整數註冊,以產生目標指令位址。連結暫存器是 rd 指定的整數暫存器。

2020 年 3 月 31 日 19..15 14 月 12 日 11..7 6..0 版
12 5 3 5 7
imm12 rs1 func3 rd 運算程式碼

如果您已看過模式,現在可以推斷 jalr 的運算子規格應為 { : rs1, imm12 : next_pc, rd },以及反組裝規格:

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

請繼續進行變更,然後開始建構。查看產生的輸出內容。和之前一樣,您可以依據 riscv32i.isarv32i_instructions.h 檢查工作。


新增商店操作說明

商店操作說明非常簡單。它們都使用 S-Type 格式:

2025 年 31 月 31 日 24..20 19..15 14 月 12 日 11..7 6..0 版
7 5 5 3 5 7
imm rs2 rs1 func3 imm 運算程式碼

如您所見,這也是另一個 12 位元立即值分割的情況,我們將其稱為 simm12。儲存指令都會將 rs2 指定的整數註冊值儲存至記憶體中的有效位址,而這個位址是將 rs1 指定的整數註冊值加到 12 位元立即值的符號延伸值後所得。所有儲存指令的運算子格式應為 { : rs1, simm12, rs2 }

需要實作的商店操作說明如下:

  • sb - 儲存位元組。
  • sh - 儲存半個字節。
  • sw - 儲存字詞。

sb 的反組規格如下:

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

語意函式規格也也是您需要的:"&RV32ISb" 等。

請繼續進行變更,然後進行建構。查看產生的輸出內容。如同先前所述,您可以根據 riscv32i.isarv32i_instructions.h 檢查您的工作。


新增載入指示

載入操作說明的模型,與模擬器中的其他指示稍有不同。為了能夠模擬載入延遲時間不確定的情況,載入指令會分為兩個獨立的動作:1) 有效位址運算和記憶體存取,以及 2) 結果回寫。在模擬器中,將負載的語意動作分成兩個不同的指示:主要指令和「子項」指令。此外,當指定運算元時,需要同時為主和子項指示指定運算元。方法是將運算元規格視為三元組清單。The syntax is:

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

如同前述操作說明,載入操作說明均使用 I-Type 格式:

31..20 19..15 14 月 12 日 11..7 6..0 版
12 5 3 5 7
imm12 rs1 func3 rd 運算程式碼

運算子規格會將運算子分割,以便計算位址,並從載入資料的註冊目的地啟動記憶體存取作業:{( : rs1, imm12 : ), ( : : rd) }

由於語意動作會分割為兩個指令,因此語意函式同樣需要指定兩個可呼叫項目。對於 lw (載入字),這會寫成:

    semfunc: "&RV32ILw", "&RV32ILwChild"

反組規格較為傳統。子項指示不會出現提及。如果是 lw,則應為:

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

需要實作的載入指示如下:

  • lb - 載入位元組。
  • lbu - 載入未簽署的位元組。
  • lh - 載入半字。
  • lhu - 載入未簽署的半字。
  • lw - 載入字詞。

請繼續進行變更,然後開始建構。查看產生的輸出內容。如同先前所述,您可以根據 riscv32i.isarv32i_instructions.h 檢查您的工作。

感謝你耐心閱讀。希望以上說明對你有幫助。