二進位檔操作說明解碼器教學課程

本教學課程的目標是:

  • 瞭解二進位格式說明檔的結構和語法。
  • 瞭解二進位格式說明如何與 ISA 說明相符。
  • 為 RiscV RV32I 部分指令撰寫二進位說明。

總覽

RiscV 二進位指令編碼

在微處理器上執行時,使用二進位檔指令編碼是將操作說明編碼的標準方式。這些檔案通常會以可執行檔儲存,通常為 ELF 格式。指令可以是固定寬度或可變寬度。

指令通常會使用一小組編碼格式,每個格式都會針對所編碼的指令類型進行自訂。舉例來說,寄存器-寄存器指令可能會使用一種格式,以便盡可能增加可用運算碼的數量,而寄存器-立即值指令則會使用另一種格式,以便在可用運算碼數量與可編碼立即值大小之間取得平衡。分支和跳躍指令幾乎總是使用格式,以便將立即值的大小盡可能加大,以便支援較大偏移量分支。

我們要在 RiscV 模擬器中解碼的指令所使用的指令格式如下:

R 型格式,用於註冊-註冊指令:

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

I 型格式,用於註冊即時指令、載入指令和 jalr 指令,12 位元即時指令。

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

特殊的 I-Type 格式,用於與立即指示轉移,5 位元立即:

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

U 型格式,用於長立即指令 (luiauipc),20 位元立即值:

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

B 型格式,用於條件式分支,12 位元立即值。

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 運算程式碼

J 型格式,用於 jal 指令,20 位元立即值。

31 30..21 20 19:12 11..7 6..0
1 10 1 8 5 7
imm imm imm imm 運算程式碼

S 型格式,用於儲存指令,12 位元立即值。

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

如您從這些格式中看到的,所有這些指令的長度為 32 位元,而每個格式中位於最底部的 7 位元是 Opcode 欄位。另外請注意,雖然多種格式具有相同大小的立即值,但這些位元是從指令的不同部分擷取。如畫面所示,二進位解碼器規格格式能夠充分說明

二進位編碼說明

指令的二進位編碼會以二進位格式 (.bin_fmt) 說明檔案表示。它會說明 ISA 中指令的二進位編碼,以便產生二進位格式指令解碼器。產生的解碼器會判斷運算碼、擷取運算元和即時欄位的值,以便提供前一篇教學課程中所述的 ISA 編碼解碼器所需的資訊。

在本教學課程中,我們將為 RiscV32I 指令的子集編寫二進位編碼說明檔案,以模擬小型「Hello World」程式中使用的指令。如要進一步瞭解 RiscV ISA,請參閱 Risc-V 規格{.external}。

首先開啟檔案: riscv_bin_decoder/riscv32i.bin_fmt

檔案內容分為幾個部分。

首先是 decoder 定義。

decoder RiscV32I {
  // The namespace in which code will be generated.
  namespace mpact::sim::codelab;
  // The name (including any namespace qualifiers) of the opcode enum type.
  opcode_enum = "OpcodeEnum";
  // Include files specific to this decoder.
  includes {
    #include "riscv_isa_decoder/solution/riscv32i_decoder.h"
  }
  // Instruction groups for which to generate decode functions.
  RiscVInst32;
};

我們的解碼器定義指定瞭解碼器 RiscV32I 的名稱,以及另外四個資訊。第一個是 namespace,用來定義產生代碼的命名空間。第二個是 opcode_enum,用於命名由 ISA 解碼器產生的運算碼列舉類型,以便在產生的程式碼中參照。第三,includes {} 會指定為此解碼器產生的程式碼所需的包含檔案。在這個例子中,這是由 ISA 解碼器在上一個教學課程中產生的檔案。您可以在全域範圍的 includes {} 定義中指定其他包含檔案。如果定義了多個解碼器,且所有解碼器都必須包含相同檔案,這個方法就很實用。第四個是指令群組的名稱清單,其中包含產生解碼器的操作說明。在我們的例子中,只有一個:RiscVInst32

接下來是三個格式定義。這些代表 32 位元指令字的不同指令格式,已定義在檔案中的指令會使用這些格式。

// The generic RiscV 32 bit instruction format.
format Inst32Format[32] {
  fields:
    unsigned bits[25];
    unsigned opcode[7];
};

// RiscV 32 bit instruction format used by a number of instructions
// needing a 12 bit immediate, including CSR instructions.
format IType[32] : Inst32Format {
  fields:
    signed imm12[12];
    unsigned rs1[5];
    unsigned func3[3];
    unsigned rd[5];
    unsigned opcode[7];
};

// RiscV instruction format used by fence instructions.
format Fence[32] : Inst32Format {
  fields:
    unsigned fm[4];
    unsigned pred[4];
    unsigned succ[4];
    unsigned rs1[5];
    unsigned func3[3];
    unsigned rd[5];
    unsigned opcode[7];
};

第一個定義了名為 Inst32Format 的 32 位元寬指令格式,其中包含兩個欄位:bits (寬度 25 位元) 和 opcode (寬度 7 位元)。每個欄位都是 unsigned,表示在擷取並放入 C++ 整數類型時,值會進行零延伸。位元欄的寬度總和必須等於格式的寬度。如果不一致,工具就會產生錯誤。這個格式不會衍生自任何其他格式,因此屬於頂層格式

第二個定義會定義名為 IType 的 32 位元寬指令格式,該格式源自 Inst32Format,因此這兩種格式相關聯。格式包含 5 個欄位:imm12rs1func3rdopcodeimm12 欄位是 signed,這表示當值擷取並放入 C++ 整數類型時,值會進行符號延伸。請注意,IType.opcode 都具有相同的正負號/未簽署屬性,且參照與 Inst32Format.opcode 相同的指令文字位元。

第三種格式是只有 fence 指令使用的自訂格式,也就是已指定的指令,我們不必擔心本教學課程中的問題。

重點:只要欄位名稱代表相同位元且具有相同的正負號屬性,即可重複使用不同相關格式的欄位名稱。

riscv32i.bin_fmt 中的格式定義之後,會出現指令群組定義。指令群組中的所有指令都必須具有相同的位元長度,並使用從相同頂層指令格式 (可能間接) 衍生的格式。如果 ISA 可包含不同長度的指令,則會為每個長度使用不同的指令群組。此外,如果目標 ISA 解碼取決於執行模式 (例如 Arm 與 Thumb 指令),則每個模式都需要個別的指令群組。bin_fmt 剖析器會為每個指令群組產生二進位解碼器。

instruction group RiscV32I[32] "OpcodeEnum" : Inst32Format {
  fence   : Fence  : func3 == 0b000, opcode == 0b000'1111;
  csrs    : IType  : func3 == 0b010, rs1 != 0, opcode == 0b111'0011;
  csrw_nr : IType  : func3 == 0b001, rd == 0,  opcode == 0b111'0011;
  csrs_nw : IType  : func3 == 0b010, rs1 == 0, opcode == 0b111'0011;
};

指令群組會定義名稱 RiscV32I、寬度 [32]、要使用的操作碼列舉類型名稱 "OpcodeEnum",以及基礎指令格式。運算碼列舉類型應與 ISA 解碼器教學課程中說明的格式獨立指令解碼器產生的類型相同。

每個指令編碼說明包含 3 個部分:

  • 指令碼名稱,必須與指令解碼器說明中使用的名稱相同,才能讓兩者搭配運作。
  • 要用於指令碼的指令格式。這種格式會用來滿足最終部分對 Bitfields 的參照。
  • 以半形逗號分隔的位元欄位限制清單,==!=<<=>>= 都必須為 true,才能讓操作碼成功比對指令字。

.bin_fmt 剖析器會使用所有這些資訊來建構解碼器,以便執行下列操作:

  • 針對每種格式中的每個位元欄位,提供適當的擷取函式 (已簽署/未簽署)。擷取器函式會放置在以格式名稱的蛇形命名版本命名的命名空間中。舉例來說,格式 IType 的擷取函式會放置在命名空間 i_type 中。每個擷取函式都會宣告 inline,並使用最窄的 uint_t 型別來保存格式的寬度,並傳回最窄的 int_t (如果是帶正負號) 或 uint_t (如果是無正負號) 型別,用於保存擷取的欄位寬度。例如:
inline uint8_t ExtractOpcode(uint32_t value) {
  return value & 0x7f;
}
  • 每個指令群組的解碼函式。它會傳回 OpcodeEnum 類型的值,並採用最窄的 uint_t 類型,該類型會保留指令群組格式的寬度。

執行初始建構

將目錄變更為 riscv_bin_decoder,然後使用下列指令建構專案:

$ cd riscv_bin_decoder
$ bazel build :all

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

$ cd ..
$ cd bazel-out/k8-fastbuild/bin/riscv_bin_decoder
  • riscv32i_bin_decoder.h
  • riscv32i_bin_decoder.cc

產生的標頭檔案 (.h)

開啟 riscv32i_bin_decoder.h。檔案的第一部分包含標準的模板防護措施、包含檔案和命名空間宣告。下方是命名空間 internal 中的範本輔助函式。這個函式可用於從格式中擷取位元欄位,這些格式太長,無法放入 64 位元 C++ 整數。

#ifndef RISCV32I_BIN_DECODER_H
#define RISCV32I_BIN_DECODER_H

#include <iostream>
#include <cstdint>

#include "third_party/absl/functional/any_invocable.h"


#include "learning/brain/research/mpact/sim/codelab/riscv_isa_decoder/solution/riscv32i_decoder.h"

namespace mpact {
namespace sim {
namespace codelab {


namespace internal {

template <typename T>
static inline T ExtractBits(const uint8_t *data, int data_size,
                            int bit_index, int width) {
  if (width == 0) return 0;

  int byte_pos = bit_index >> 3;
  int end_byte = (bit_index + width - 1) >> 3;
  int start_bit = bit_index & 0x7;

  // If it is only from one byte, extract and return.
  if (byte_pos == end_byte) {
    uint8_t mask = 0xff >> start_bit;
    return (mask & data[byte_pos]) >> (8 - start_bit - width);
  }

  // Extract from the first byte.
  T val = 0;
  val = data[byte_pos++] & 0xff >> start_bit;
  int remainder = width - (8 - start_bit);
  while (remainder >= 8) {
    val = (val << 8) | data[byte_pos++];
    remainder -= 8;
  }

  // Extract any remaining bits.
  if (remainder > 0) {
    val <<= remainder;
    int shift = 8 - remainder;
    uint8_t mask = 0b1111'1111 << shift;
    val |= (data[byte_pos] & mask) >> shift;
  }
  return val;
}

}  // namespace internal

在初始部分之後,有三個命名空間組合,每個命名空間對應至 riscv32i.bin_fmt 檔案中的每個 format 宣告:


namespace fence {

...

}  // namespace fence

namespace i_type {

...

}  // namespace i_type

namespace inst32_format {

...

}  // namespace inst32_format

在每個命名空間中,會定義該格式中每個位元欄位的 inline 位元欄位擷取函式。此外,基本格式會從子項格式複製擷取函式,這些子項格式會在以下情況下發生:1) 欄位名稱只出現在單一欄位名稱中,或 2) 欄位名稱在每個格式中參照相同類型的欄位 (已簽署/未簽署和位元位置)。這樣一來,系統就能使用頂層格式命名空間中的函式,擷取描述相同位元的位元欄位。

以下是 i_type 命名空間中的函式:

namespace i_type {

inline uint8_t ExtractFunc3(uint32_t value) {
  return  (value >> 12) & 0x7;
}

inline int16_t ExtractImm12(uint32_t value) {
  int16_t result = ( (value >> 20) & 0xfff) << 4;
  result = result >> 4;
  return result;
}

inline uint8_t ExtractOpcode(uint32_t value) {
  return value & 0x7f;
}

inline uint8_t ExtractRd(uint32_t value) {
  return  (value >> 7) & 0x1f;
}

inline uint8_t ExtractRs1(uint32_t value) {
  return  (value >> 15) & 0x1f;
}

}  // namespace i_type

最後,系統會宣告指令群組 RiscVInst32 中解碼器函式的函式宣告。這個 ID 會採用 32 位元的未簽署值做為指令字詞的值,並傳回相符的 OpcodeEnum 列舉類別成員;如果沒有相符項目,則傳回 OpcodeEnum::kNone

OpcodeEnum DecodeRiscVInst32(uint32_t inst_word);

產生的來源檔案 (.cc)

接著開啟 riscv32i_bin_decoder.cc。檔案的第一個部分包含 #include 和命名空間宣告,隨後是解碼器函式宣告:

#include "riscv32i_bin_decoder.h"

namespace mpact {
namespace sim {
namespace codelab {

OpcodeEnum DecodeRiscVInst32None(uint32_t);
OpcodeEnum DecodeRiscVInst32_0(uint32_t inst_word);
OpcodeEnum DecodeRiscVInst32_0_3(uint32_t inst_word);
OpcodeEnum DecodeRiscVInst32_0_3c(uint32_t inst_word);
OpcodeEnum DecodeRiscVInst32_0_5c(uint32_t inst_word);

DecodeRiscVInst32None 用於空白解碼動作,也就是傳回 OpcodeEnum::kNone 的動作。其他三個函式則組成產生的解碼器。整體解碼器會以階層方式運作。系統會計算指令字詞中的位元組,以區分頂層的指令或指令群組。位元不需要連續。位元數決定了查詢表的大小,該表會填入第二層解碼器函式。請參閱檔案的下一節:

absl::AnyInvocable<OpcodeEnum(uint32_t)> parse_group_RiscVInst32_0[kParseGroupRiscVInst32_0_Size] = {
    &DecodeRiscVInst32None, &DecodeRiscVInst32None,
    &DecodeRiscVInst32None, &DecodeRiscVInst32_0_3,
    &DecodeRiscVInst32None, &DecodeRiscVInst32None,

    ...

    &DecodeRiscVInst32None, &DecodeRiscVInst32None,
    &DecodeRiscVInst32None, &DecodeRiscVInst32None,
    &DecodeRiscVInst32_0_3c, &DecodeRiscVInst32None,

    ...
};

最後,定義解碼器函式:

OpcodeEnum DecodeRiscVInst32None(uint32_t) {
  return OpcodeEnum::kNone;
}

OpcodeEnum DecodeRiscVInst32_0(uint32_t inst_word) {
  if ((inst_word & 0x4003) != 0x3) return OpcodeEnum::kNone;
  uint32_t index;
  index = (inst_word >> 2) & 0x1f;
  index |= (inst_word >> 7) & 0x60;
  return parse_group_RiscVInst32_0[index](inst_word);
}

OpcodeEnum DecodeRiscVInst32_0_3(uint32_t inst_word) {
  return OpcodeEnum::kFence;
}

OpcodeEnum DecodeRiscVInst32_0_3c(uint32_t inst_word) {
  if ((inst_word & 0xf80) != 0x0) return OpcodeEnum::kNone;
  return OpcodeEnum::kCsrwNr;
}

OpcodeEnum DecodeRiscVInst32_0_5c(uint32_t inst_word) {
  uint32_t rs1_value = (inst_word >> 15) & 0x1f;
  if (rs1_value != 0x0)
    return OpcodeEnum::kCsrs;
  if (rs1_value == 0x0)
    return OpcodeEnum::kCsrsNw;
  return OpcodeEnum::kNone;
}

OpcodeEnum DecodeRiscVInst32(uint32_t inst_word) {
  OpcodeEnum opcode;
  opcode = DecodeRiscVInst32_0(inst_word);
  return opcode;
}

在本例中,只有 4 個指令定義,因此只有單一層級的解碼作業,以及非常稀疏的查詢表。新增指示後,解碼器的結構將會變更,解碼器表格階層中的層級數量也可能會增加。


新增註冊-註冊 ALU 指令

接著,請在 riscv32i.bin_fmt 檔案中新增一些指令。第一組指令是寄存器-寄存器 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 運算程式碼

首先,我們需要新增格式。請在您偏好的編輯器中開啟 riscv32i.bin_fmtInst32Format 之後,您可以新增格式 RType,該格式源自 Inst32FormatRType 中的所有位元欄都是 unsigned。請使用上表中的名稱、位元寬度和順序 (從左到右) 定義格式。如需提示或查看完整解決方案,請按這裡

接下來,我們需要新增操作說明。操作說明如下:

  • add - 整數加法。
  • and:位元和。
  • or - 位元或。
  • sll - 邏輯左移。
  • sltu - 設定小於值,未簽署。
  • sub - 整數減法。
  • xor - 位元 xor。

其編碼如下:

31..25 24..20 19..15 14..12 11..7 6..0 opcode 名稱
000 0000 rs2 rs1 000 rd 011 0011 add
000 0000 rs2 rs1 111 rd 011 0011
000 0000 rs2 rs1 110 011 0011
000 0000 rs2 rs1 001 rd 011 0011 sll
000 0000 rs2 rs1 011 rd 011 0011 sltu
010 0000 rs2 rs1 000 rd 011 0011 替補球員
000 0000 rs2 rs1 100 011 0011 xor
func7 func3 運算程式碼

請在 RiscVInst32 指令群組中的其他指令之前加入這些指令定義。二進位字串會以 0b 做為開頭前置字元 (類似十六進位數的 0x)。為了讓長串二進位數字更容易閱讀,您也可以視需要在適當位置插入單引號 ' 做為數字分隔符。

每個指令定義都會有三個限制,分別是 func7func3opcode。除了 sub 以外,所有 func7 限制都會是:

func7 == 0b000'0000

func3 限制會因大多數操作說明而異。addsub 的值為:

func3 == 0b000

以下各項指令的 opcode 限制條件相同:

opcode == 0b011'0011

請記得在每行結尾加上半形分號 ;

完成的解決方案請見這裡

接著,請按照先前的方式建構專案,然後開啟產生的 riscv32i_bin_decoder.cc 檔案。您會發現系統已產生其他解碼器函式,用於處理新指令。這些函式大多與先前產生的函式相似,但請查看用於 add/sub 解碼的 DecodeRiscVInst32_0_c

OpcodeEnum DecodeRiscVInst32_0_c(uint32_t inst_word) {
  static constexpr OpcodeEnum opcodes[2] = {
    OpcodeEnum::kAdd,
    OpcodeEnum::kSub,
  };
  if ((inst_word & 0xbe000000) != 0x0) return OpcodeEnum::kNone;
  uint32_t index;
  index = (inst_word >> 30) & 0x1;
  return opcodes[index];
}

這個函式會產生靜態解碼表,並從指令字取出查詢值,以選取適當的索引。這樣會在操作說明解碼器階層中新增第二層,但由於運算程式碼可直接查詢在表格中,不會進一步比較,因此會內嵌在這個函式中,而非需要其他函式呼叫。


新增含立即值的 ALU 指令

我們接下來要加入的下一個指令集是 ALU 指令,這類指令會使用立即值,而非其中一個暫存器。這類指令分為三組 (根據立即值欄位):I 型立即值指令 (具有 12 位元帶符號立即值)、專門用於位移的 I 型立即值指令,以及 U 型立即值 (具有 20 位元無符號立即值)。格式如下:

I 型立即格式:

31..20 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 運算程式碼

riscv32i.bin_fmt 中已存在 I 型格式,因此不需要新增該格式。

如果將專屬的 I 型格式與我們在前一個練習中定義的 R 型格式進行比較,我們會發現唯一的差異在於 rs2 欄位已重新命名為 uimm5。我們可以擴充 R 型格式,而非新增全新格式。我們無法新增另一個欄位,因為它會增加格式的寬度,但您可以新增「疊加層」疊加層是格式中一組位元的別名,可用於將格式的多個子序組合成個別命名的實體。副作用是,除了欄位之外,產生的程式碼現在也會包含疊加層的擷取函式。在這種情況下,如果 rs2uimm5 都未簽署,則除了明確指出該欄位用於立即值之外,不會有太大差異。如要在 R-Type 格式中新增名為 uimm5 的疊加層,請在最後一個欄位後方加入以下內容:

  overlays:
    unsigned uimm5[5] = rs2;

我們只需要新增 U 型格式。在新增格式之前,請先考慮使用該格式的兩個指令:auipclui。這兩種方法都會將 20 位元立即值左移 12 位元,然後用於將 pc 加到其中 (auipc),或直接寫入註冊 (lui)。我們可以使用疊加層提供預先移位版本的立即值,將一小部分運算從指令執行移至指令解碼。首先,請根據上表中指定的欄位新增格式。接著,我們可以新增下列疊加層:

  overlays:
    unsigned uimm32[32] = uimm20, 0b0000'0000'0000;

重疊語法可讓我們連結欄位,以及文字常值。在本例中,我們會將其與 12 個零連接,實際上會將其向左移動 12 位元。

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

  • addi - 立即加入。
  • andi - 位元 and 與立即運算子。
  • ori - 位元或運算與立即值。
  • xori - 位元 xor 與立即值。

其編碼如下:

31..20 19:15 14..12 11..7 6..0 opcode_name
imm12 rs1 000 001 0011 addi
imm12 rs1 111 rd 001 0011 andi
imm12 rs1 110 001 0011 ori
imm12 rs1 100 001 0011 Xori
func3 運算程式碼

我們需要新增的 R-Type (專門的 I-Type) 指令如下:

  • slli - 立即將左移邏輯列。
  • srai - 以立即值右移算術。
  • srli - 立即右移邏輯。

其編碼如下:

31..25 24..20 19..15 14..12 11..7 6..0 opcode 名稱
000 0000 uimm5 rs1 001 rd 001 0011 slli
010,0000 人 uimm5 rs1 101 001 0011 srai
000 0000 uimm5 rs1 101 001 0011 srli
func7 func3 運算程式碼

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

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

其編碼如下:

12 月 31 日 11..7 6..0 Opcode 名稱
uimm20 001 0111 auipc
uimm20 011 0111 lui
運算程式碼

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


接下來要定義的另一組指令是條件分支指令、跳轉及連結指令,以及跳轉及連結暫存器指令。

我們新增的條件分支都使用 B 型編碼。

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

雖然 B 型編碼的版面配置與 R 型編碼相同,但我們選擇使用新的格式類型,以便與 RiscV 說明文件保持一致。不過,您也可以使用 R-Type 編碼的 func7rd 欄位,新增疊加層,以便立即取得適當的分支位移。

您必須新增格式 BType,並加入上述指定的欄位,但這還不夠。如您所見,立即值會分散在兩個指令欄位。此外,分支指令不會將這項作業視為兩個欄位的簡單串連作業。相反地,每個欄位都會進一步分割,然後以不同的順序串連。最後,該值會向左移動,取得 16 位元對齊的位移。

用於形成立即值的指令字元序列為:31, 7, 30..25, 11..8。這會對應到下列子欄位參照,其中索引或範圍會指定欄位中的位元,從右到左編號,也就是imm7[6]imm7 的 MSB,imm5[0]imm5 的 MSB。

imm7[6], imm5[0], imm7[5..0], imm5[4..1]

將此位元操作納入分支指令本身,有兩個重大缺點。首先,它會將語意函式的實作內容與二進位指令表示法中的詳細資料綁定。第二,這樣會增加執行階段負擔答案是將疊加層新增至 BType 格式,包括尾隨的「0」來計算左移。

  overlays:
    signed b_imm[13] = imm7[6], imm5[0], imm7[5..0], imm5[4..1], 0b0;

請注意,疊加層已簽署,因此在從指令字詞中擷取時,會自動進行簽署延伸。

跳轉及連結 (立即) 指令會使用 J 型編碼:

12 月 31 日 11..7 6..0
20 5 7
imm20 運算程式碼

這也是很容易新增的格式,但同樣地,指令所使用的立即值並非看起來那麼簡單。用於形成完整立即值的位元序列為:31、19..12、20、30..21,而最終的立即值會向左偏移一個位元組,以便半字對齊。解決方法是在格式中新增另一個疊加層 (21 位元,用於左移):

  overlays:
    signed j_imm[21] = imm20[19, 7..0, 8, 18..9], 0b0;

如您所見,疊加語法支援以簡寫格式在欄位中指定多個範圍。此外,如果未使用欄位名稱,位元數會參照指令字本身,因此上述指令也可以寫成以下形式:

    signed j_imm[21] = [31, 19..12, 20, 30..21], 0b0;

最後,跳躍和連結 (登錄) 會使用先前使用的 I 型格式。

I 型立即格式:

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

這次格式不需要進行任何變更。

需要新增的分支版本指示如下:

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

這些程式碼的編碼方式如下:

31..25 24..20 19..15 14..12 11..7 6..0 opcode 名稱
imm7 rs2 rs1 000 imm5 110 0011 beq
imm7 rs2 rs1 101 imm5 110 0011 bge
imm7 rs2 rs1 111 imm5 110 0011 Bgeu
imm7 rs2 rs1 100 imm5 110 0011 blt
imm7 rs2 rs1 110 imm5 110 0011 bltu
imm7 rs2 rs1 001 imm5 110 0011 bne
func3 運算程式碼

jal 指令的編碼方式如下:

12 月 31 日 11..7 6..0 Opcode 名稱
imm20 rd 110 1111 jal
運算程式碼

jalr 指令的編碼方式如下:

2020 年 3 月 31 日 19:15 14..12 11..7 6..0 opcode_name
imm12 rs1 000 rd 110 0111 jalr
func3 運算程式碼

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


新增商店操作說明

儲存指令使用 S 型編碼,與分支指令使用的 B 型編碼相同,除了立即值的組合。我們選擇新增 SType 格式,以便與 RiscV 說明文件保持一致。

31..25 24..20 19..15 14..12 11..7 6..0
7 5 5 3 5 7
imm7 rs2 rs1 func3 imm5 運算程式碼

SType 格式中,直接值是兩個直接值欄位的直線串連,因此疊加規格很簡單:

  overlays:
    signed s_imm[12] = imm7, imm5;

請注意,連結整個欄位時,不需要位元範圍指定符。

儲存指令的編碼方式如下:

31..25 24..20 19..15 14..12 11..7 6..0 opcode 名稱
imm7 rs2 rs1 000 imm5 010 0011 sb
imm7 rs2 rs1 001 imm5 010 0011 sh
imm7 rs2 rs1 010 imm5 010 0011 sw
func3 運算程式碼

請繼續進行變更,然後進行建構。檢查產生的輸出內容。和之前一樣,您可以對 riscv32i.bin_fmt 檢查工作。


新增載入指示

載入指令使用 I 型格式。因此不需要在該處進行任何變更。

編碼方式如下:

31..20 19:15 14..12 11..7 6..0 opcode_name
imm12 rs1 000 rd 000 0011 lb
imm12 rs1 100 000 0011 lbu
imm12 rs1 001 000 0011 lh
imm12 rs1 101 rd 000 0011 lhu
imm12 rs1 010 000 0011 lw
func3 運算程式碼

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

本教學課程到此結束,希望對您有所幫助。