二进制指令解码器教程

本教程的目标如下:

  • 了解二进制格式说明文件的结构和语法。
  • 了解二进制格式说明如何与 ISA 说明匹配。
  • 为 RiscV RV32I 指令子集编写二进制说明。

概览

RiscV 二进制指令编码

二进制指令编码是编码指令以便在微处理器上执行的标准方法。它们通常存储在可执行文件中,通常采用 ELF 格式。指令可以是固定宽度或可变宽度。

通常,指令使用一小组编码格式,每种格式都针对编码的指令类型进行自定义。例如,寄存器注册指令可以使用一种使可用操作码数量最大化的格式,而寄存器即时指令使用另一种格式来牺牲可用操作码的数量来增加可编码的立即数的大小。分支和跳转指令几乎始终使用最大限度地扩大立即数大小的格式,以支持偏移量较大的分支。

我们要在 RiscV 模拟器中解码的指令使用的指令格式如下:

R 型格式,用于寄存器-寄存器指令:

31 月 25 日 24..20 19..15 12 月 14 日 11..7 6.0
7 5 5 3 5 7
func7 rs2 rs1 func3 rd 操作码

I 类型格式,用于寄存器立即指令、加载指令和 jalr 指令,12 位立即数。

2020 年 31 月 20 日 19..15 12 月 14 日 11..7 6..0
12 5 3 5 7
imm12 rs1 func3 rd 操作码

专用 I-Type 格式,用于带立即数的指令,5 位立即数:

31..25 24..20 19..15 12 月 14 日 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 21 月 30 日 20 19..12 11..7 6..0
1 10 1 8 5 7
imm imm imm imm rd 操作码

S 类型格式,用于存储指令,12 位立即数。

31..25 24..20 19..15 12 月 14 日 11..7 6.0
7 5 5 3 5 7
imm rs2 rs1 func3 imm 操作码

从这些格式可以看出,所有这些指令的长度均为 32 位,并且每种格式的低 7 位是操作码字段。另请注意,虽然有几种格式具有相同大小的立即数,但其位数取自指令的不同部分。正如我们将要看到的,二进制解码器规范格式能够表达这一点。

二进制编码说明

指令的二进制编码以二进制格式 (.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 {} 定义中指定其他 include 文件。如果定义了多个解码器,并且它们都需要包含一些相同的文件,这将非常有用。第四项是指构成要为其生成解码器的指令的指令组的名称列表。在本例中,只有一个: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 部分组成:

  • 操作码名称,必须与指令解码器说明中所使用的相同,才能使两者协同工作。
  • 要为操作码使用的指令格式。此格式用于满足最终部分对位字段的引用。
  • 以逗号分隔的位字段约束条件列表,其中包含 ==!=<<=>>=,所有这些约束条件都必须为真,操作码才能成功匹配指令字。

.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 的解码器函数的函数声明。它将 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 文件添加一些新指令。第 1 组指令是寄存器-寄存器 ALU 指令,例如 addand 等。在 RiscV32 上,这些指令都使用 R 型二进制指令格式:

31..25 24..20 19..15 12 月 14 日 11..7 6.0
7 5 5 3 5 7
func7 rs2 rs1 func3 rd 操作码

首先,我们需要添加格式。接下来,在您惯用的编辑器中打开 riscv32i.bin_fmt。在 Inst32Format 后面,添加一个名为 RType 的格式,该格式派生自 Inst32FormatRType 中的所有位字段均为 unsigned。使用上表中的名称、位宽和顺序(从左到右)来定义格式。如果您需要提示或想查看完整解题步骤,请点击此处

接下来,我们需要添加说明。具体说明如下:

  • add - 整数加法。
  • and - 按位和。
  • or - 按位或。
  • sll - 逻辑左移。
  • sltu - 设置小于,无符号。
  • sub - 整数相减。
  • xor - 按位异或。

其编码如下:

31 月 25 日 24..20 19..15 12 月 14 日 11..7 6..0 操作码名称
000 0000 rs2 rs1 000 rd 011 0011 add
000 0000 rs2 rs1 111 rd 011,0011
000 0000 rs2 rs1 110 rd 011 0011
000 0000 rs2 rs1 001 rd 011 0011 sll
000 0000 rs2 rs1 011 RD 011 0011 斯尔图
010 0000 rs2 rs1 000 rd 011 0011 sub
00 万 rs2 rs1 100 rd 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 指令。这类指令分为三组(基于立即字段):具有 12 位有符号立即数的 I 型立即数指令、用于移位的专用 I 型立即数指令,以及具有 20 位无符号立即数值的 U 型立即数。格式如下所示:

I 类型立即格式:

2020 年 31 月 20 日 19..15 12 月 14 日 11..7 6..0
12 5 3 5 7
imm12 rs1 func3 rd 操作码

专用 I-Type 立即格式:

31..25 24..20 19..15 12 月 14 日 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 操作码

I 型格式已存在于 riscv32i.bin_fmt 中,因此无需添加该格式。

如果我们将专用 I-Type 格式与我们在上一个练习中定义的 R-Type 格式进行比较,就会发现唯一的区别是 rs2 字段已重命名为 uimm5。我们可以扩展 R 型格式,而不是添加全新格式。我们无法添加其他字段,因为这会增加格式的宽度,但我们可以添加叠加层叠加层是格式中一组位元的别名,可用于将格式的多个子序列组合到单独的命名实体中。副作用是,生成的代码现在除了包含字段的提取函数外,还会包含叠加层的提取函数。在这种情况下,当 rs2uimm5 均为无符号时,除了明确说明字段用作立即值之外,没有太大区别。如需向 R 类型格式添加名为 uimm5 的叠加层,请在最后一个字段后面添加以下内容:

  overlays:
    unsigned uimm5[5] = rs2;

我们只需要添加 U 型格式。在添加该格式之前,我们先来考虑一下使用该格式的两个指令:auipclui。这两个命令都会先将 20 位立即值向左移动 12,然后再使用该值将 pc 添加到其中 (auipc) 或将其直接写入寄存器 (lui)。使用叠加层,我们可以提供立即转移的预移版本,将一些计算从指令执行转移到指令解码。首先,根据上表中指定的字段添加格式。然后,我们可以添加以下叠加层:

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

借助叠加层语法,我们不仅可以串联字段,还可以串联字面量。在本例中,我们用 12 个 0 串联起来,实际上将它左移 12。

我们需要添加的 I 型说明如下:

  • addi - 立即添加。
  • andi - 按位立即,
  • ori - 带立即数的按位或。
  • xori - 按位异或立即数。

其编码为:

31..20 19..15 12 月 14 日 11..7 6..0 opcode_name
imm12 rs1 000 RD 001 0011 addi
imm12 rs1 111 rd 001 0011 andi
imm12 rs1 110 rd 001 0011 ori
imm12 rs1 100 RD 001 0011 xori
func3 操作码

我们需要添加的 R-Type(专用 I-Type)指令如下:

  • slli - 按立即值逻辑左移。
  • srai - 按立即数右移算术。
  • srli - 按立即数逻辑右移。

其编码如下:

31 月 25 日 24..20 19..15 12 月 14 日 11..7 6..0 操作码名称
000 0000 uimm5 rs1 001 RD 001 0011 slli
010 万 uimm5 rs1 101 rd 001 0011 srai
000 0000 uimm5 rs1 101 rd 001 0011 srli
func7 func3 操作码

我们需要添加的 U 型指令如下:

  • auipc - 向 pc 添加了上位立即数。
  • lui - 立即加载上层。

其编码为:

12 月 31 日 11..7 6..0 操作码名称
uimm20 rd 001 0111 auipc
uimm20 RD 011 0111 lui
操作码

请继续进行更改,然后进行构建。检查生成的输出。与之前一样,您可以对照 riscv32i.bin_fmt 检查自己的工作。


需要定义的下一组指令是条件分支指令、跳转-链接指令以及跳转-链接寄存器指令。

我们添加的条件分支都使用 B 类型编码。

31..25 24..20 19..15 12 月 14 日 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 的 lsb。

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 rd 操作码

这也是一种易于添加的格式,但同样,指令使用的立即数并不像看起来那么简单。用于构成完整立即数的比特序列为: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-Type 直接格式:

2020 年 31 月 20 日 19..15 12 月 14 日 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 12 月 14 日 11..7 6..0 操作码名称
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 操作码名称
imm20 rd 110 1111 贾勒
操作码

jalr 指令的编码如下所示:

2020 年 31 月 20 日 19..15 12 月 14 日 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 12 月 14 日 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 12 月 14 日 11..7 6..0 操作码名称
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-Type 格式。无需进行任何更改。

编码为:

2020 年 31 月 20 日 19..15 12 月 14 日 11..7 6..0 opcode_name
imm12 rs1 000 rd 000 0011 lb
imm12 rs1 100 rd 000 0011 lbu
imm12 rs1 001 rd 000 0011 小时
imm12 rs1 101 rd 000 0011 lhu
imm12 rs1 010 RD 000 0011 lw
func3 操作码

请继续进行更改,然后进行构建。检查生成的输出。与之前一样,您可以对照 riscv32i.bin_fmt 检查自己的工作。

本教程到此结束,希望对您有所帮助。