RiscV ISA-декодер

Цели этого руководства:

  • Узнайте, как инструкции представлены в симуляторе MPACT-Sim.
  • Изучите структуру и синтаксис файла описания ISA.
  • Напишите описания ISA для подмножества инструкций RiscV RV32I.

Обзор

В MPACT-Sim целевые инструкции декодируются и сохраняются во внутреннем представлении, что делает их информацию более доступной и ускоряет выполнение семантики. Эти экземпляры инструкций кэшируются в кэше инструкций, чтобы уменьшить количество выполнения часто выполняемых инструкций.

Класс обучения

Прежде чем мы начнем, полезно немного взглянуть на то, как инструкции представлены в MPACT-Sim. Класс Instruction определен в mpact-sim/mpact/sim/generic/instruction.h .

Экземпляр класса Instruction содержит всю информацию, необходимую для имитации инструкции при ее «выполнении», например:

  1. Адрес инструкции, размер моделируемой инструкции, т. е. размер в формате .text.
  2. Код операции инструкции.
  3. Указатель интерфейса операнда предиката (если применимо).
  4. Вектор указателей интерфейса исходного операнда.
  5. Вектор указателей интерфейса операнда назначения.
  6. Вызываемая семантическая функция.
  7. Указатель на архитектурный государственный объект.
  8. Указатель на объект контекста.
  9. Указатель на дочерний и следующий экземпляры инструкций.
  10. Разборочная строка.

Эти экземпляры обычно хранятся в кэше инструкций (экземпляров) и используются повторно при каждом повторном выполнении инструкции. Это повышает производительность во время выполнения.

За исключением указателя на объект контекста, все они заполняются декодером инструкций, который генерируется из описания ISA. Для этого урока нет необходимости знать детали этих элементов, поскольку мы не будем использовать их напрямую. Вместо этого достаточно высокого уровня понимания того, как они используются.

Вызываемая семантическая функция — это объект функции/метода/функции C++ (включая лямбда-выражения), который реализует семантику инструкции. Например, инструкция add загружает каждый исходный операнд, складывает два операнда и записывает результат в один целевой операнд. Тема семантических функций подробно рассматривается в руководстве по семантическим функциям.

Операнды инструкций

Класс инструкций включает указатели на три типа интерфейсов операндов: предикат, источник и назначение. Эти интерфейсы позволяют писать семантические функции независимо от фактического типа базового операнда инструкции. Например, доступ к значениям регистров и непосредственных значений осуществляется через один и тот же интерфейс. Это означает, что инструкции, выполняющие одну и ту же операцию, но с разными операндами (например, регистры или непосредственные данные), могут быть реализованы с использованием одной и той же семантической функции.

Интерфейс операнда предиката для тех ISA, которые поддерживают выполнение предикатных инструкций (для других 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 (архитектура набора команд) процессора определяет абстрактную модель, с помощью которой программное обеспечение взаимодействует с оборудованием. Он определяет набор доступных инструкций, типы данных, регистры и другие состояния машины, с которыми работают инструкции, а также их поведение (семантику). Для целей MPACT-Sim ISA не включает фактическое кодирование инструкций. Это рассматривается отдельно.

Процессор ISA выражается в файле описания, который описывает набор команд на абстрактном, независимом от кодирования уровне. Файл описания перечисляет набор доступных инструкций. Для каждой инструкции обязательно указывать ее имя, количество и имена ее операндов, а также ее привязку к функции/вызываемому объекту C++, реализующему ее семантику. Кроме того, можно указать строку форматирования дизассемблирования и использование инструкций имен аппаратных ресурсов. Первый полезен для создания текстового представления инструкции для отладки, трассировки или интерактивного использования. Последнее можно использовать для повышения точности цикла моделирования.

Файл описания ISA анализируется анализатором ISA, который генерирует код для декодера команд, не зависящего от представления. Этот декодер отвечает за заполнение полей объектов инструкций. Конкретные значения, например номер регистра назначения, получаются из декодера инструкций конкретного формата. Одним из таких декодеров является двоичный декодер, которому посвящено следующее руководство.

В этом руководстве рассказывается, как написать файл описания ISA для простой скалярной архитектуры. Чтобы проиллюстрировать это, мы будем использовать подмножество набора инструкций RiscV RV32I и вместе с другими руководствами создадим симулятор, способный имитировать программу «Hello World». Более подробную информацию о RiscV ISA см. в разделе «Спецификации Risc-V» .

Начните с открытия файла: riscv_isa_decoder/riscv32i.isa

Содержимое файла разбито на несколько разделов. Во-первых, это декларация 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};

Это указывает, что первый фрагмент дизассемблирования любой спецификации дизассемблирования (подробнее см. ниже) будет выровнен по левому краю в поле шириной 15 символов. Любые последующие фрагменты будут добавляться к этому полю без каких-либо дополнительных пробелов.

Ниже находятся три объявления слотов: riscv32i , zicsr и riscv32 . Согласно приведенному выше определению isa , частью isa RiscV32I будут только инструкции, определенные для слота riscv32 . Для чего нужны остальные два слота?

Слоты можно использовать для разделения инструкций на отдельные группы, которые затем в конце можно объединить в один слот. Обратите внимание на обозначения : riscv32i, zicsr в объявлении слота riscv32 . Это указывает, что слот riscv32 наследует все инструкции, определенные в слотах zicsr и riscv32i . 32-битная ISA RiscV состоит из базовой ISA под названием RV32I, к которой может быть добавлен набор дополнительных расширений. Механизм слотов позволяет указывать инструкции в этих расширениях отдельно, а затем объединять их по мере необходимости для определения общей ISA. В этом случае инструкции в группе RiscV «I» определяются отдельно от инструкций в группе «zicsr». Дополнительные группы могут быть определены для «M» (умножение/деление), «F» (с плавающей запятой одинарной точности), «D» (с плавающей запятой двойной точности), «C» (компактные 16-битные инструкции) и т. д. как необходимо для желаемой окончательной версии 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 {
  ...
}

Определения слотов zicsr и riscv32 изменять не нужно. Однако в этом руководстве основное внимание уделяется добавлению необходимых определений в слот 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 {} глобальной областью действия, и в этом случае они всегда включаются. Это может быть удобно, если в противном случае к каждому определению слота пришлось бы добавлять один и тот же включаемый файл.

Объявления default size и default latency определяют, что, если не указано иное, размер инструкции равен 4, а задержка записи целевого операнда равна 0 циклам. Обратите внимание: размер указанной здесь инструкции — это размер приращения счетчика программ для вычисления адреса следующей последовательной инструкции, которая будет выполнена в моделируемом процессоре. Он может совпадать, а может и не совпадать с размером в байтах представления инструкции во входном исполняемом файле.

Центральным элементом определения слота является раздел кода операции. Как видите, в riscv32i на данный момент определены только два опкода (инструкции) fence и ebreak . Код операции fence определяется путем указания имени ( fence ) и спецификации операнда ( {: imm12 : } ), за которым следует необязательный формат дизассемблирования ( "fence" ) и вызываемый объект, который должен быть связан как семантическая функция ( "&RV32IFence" ).

Операнды инструкций задаются в виде тройки, где каждый компонент отделяется точкой с запятой, предикат ':' список исходных операндов ':' список операндов назначения . Списки операндов источника и назначения представляют собой списки имен операндов, разделенных запятыми. Как вы можете видеть, операнды инструкции для инструкции fence не содержат операндов-предикатов, только одно имя исходного операнда imm12 и не содержат операндов-адресатов. Подмножество RiscV RV32I не поддерживает предикатное выполнение, поэтому в этом руководстве операнд предиката всегда будет пустым.

Семантическая функция указывается как строка, необходимая для указания функции C++ или вызываемая для использования для вызова семантической функции. Сигнатура семантической функции/вызываемого объекта — void(Instruction *) .

Спецификация дизассемблирования состоит из списка строк, разделенных запятыми. Обычно используются только две строки: одна для кода операции и одна для операндов. При форматировании (с использованием вызова AsString() в инструкции) каждая строка форматируется внутри поля в соответствии со спецификацией disasm widths описанной выше.

Следующие упражнения помогут вам добавить в файл riscv32i.isa инструкции, достаточные для имитации программы «Hello World». Для тех, кто спешит, решения можно найти в riscv32i.isa и rv32i_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 , в котором хранятся все имена кодов операций (он определен в riscv32i_enums.cc ). Остальные файлы содержат сгенерированный декодер, который будет подробно рассмотрен в другом руководстве.

Правило сборки Базеля

Целевой объект декодера ISA в Bazel определяется с помощью макроса настраиваемого правила с именем mpact_isa_decoder , который загружается из mpact/sim/decoder/mpact_sim_isa.bzl в репозитории mpact-sim . В этом руководстве цель сборки, определенная в 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_isa . Раздел includes используется для указания дополнительных файлов .isa , которые может включать исходный файл. isa_name используется для указания того, какая именно isa требуется, если указано более одного, в исходном файле, для которого нужно сгенерировать декодер.


Добавить инструкции регистр-регистр ALU

Теперь пришло время добавить несколько новых инструкций в файл riscv32i.isa . Первая группа инструкций — это инструкции ALU «регистр-регистр», такие как add , and и т. д. В RiscV32 все они используют формат двоичных инструкций R-типа:

31..25 24..20 19..15 14..12 11..7 6..0
7 5 5 3 5 7
функция7 rs2 rs1 функция3 р-д код операции

Хотя файл .isa используется для создания независимого от формата декодера, все же полезно учитывать двоичный формат и его структуру для управления записями. Как видите, есть три поля, которые относятся к декодеру, заполняющему объекты инструкций: rs2 , rs1 и rd . На этом этапе мы решим использовать эти имена для целочисленных регистров, которые кодируются одинаково (битовые последовательности), в одних и тех же полях инструкций, во всех инструкциях.

Инструкции, которые мы собираемся добавить, следующие:

  • add - целое число add.
  • and - побитовое и.
  • or - побитовое или.
  • sll — логический сдвиг влево.
  • sltu — установить меньше, без знака.
  • sub - вычитание целого числа.
  • xor — побитовое исключающее ИЛИ.

Каждая из этих инструкций будет добавлена ​​в раздел opcodes определения слота riscv32i . Напомним, что нам нужно указать имя, коды операций, дизассемблирование и семантическую функцию для каждой инструкции. Имя простое, давайте просто воспользуемся названиями кодов операций, приведенными выше. Кроме того, все они используют одни и те же операнды, поэтому мы можем использовать { : 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 с помощью Immediates

Следующий набор инструкций, который мы добавим, — это инструкции ALU, которые используют непосредственное значение вместо одного из регистров. Существует три группы этих инструкций (в зависимости от поля немедленного действия): немедленные инструкции I-типа с 12-битным немедленным знаком, специализированные немедленные инструкции I-типа для сдвигов и немедленные инструкции U-типа с 20-битным немедленным полем. беззнаковое немедленное значение. Форматы показаны ниже:

Непосредственный формат I-Type:

31..20 19..15 14..12 11..7 6..0
12 5 3 5 7
имм12 rs1 функция3 р-д код операции

Специализированный непосредственный формат I-Type:

31..25 24..20 19..15 14..12 11..7 6..0
7 5 5 3 5 7
функция7 uimm5 rs1 функция3 р-д код операции

Немедленный формат U-Type:

31..12 11..7 6..0
20 5 7
uimm20 р-д код операции

Как видите, имена операндов rs1 и rd относятся к тем же битовым полям, что и ранее, и используются для представления целочисленных регистров, поэтому эти имена можно сохранить. Поля непосредственных значений имеют разную длину и расположение, два из них ( uimm5 и uimm20 ) беззнаковые, а imm12 — со знаком. Каждый из них будет использовать свое имя.

Поэтому операндами для инструкций I-Type должны быть { : rs1, imm12 :rd } . Для специализированных инструкций I-Type это должно быть { : rs1, uimm5 : rd} . Спецификация операнда инструкции U-типа должна быть { : uimm20 : rd } .

Инструкции I-Type, которые нам нужно добавить:

  • addi — Добавить немедленно.
  • andi — Побитовый и с немедленным.
  • ori — Побитовый или с немедленным.
  • xori — побитовое исключающее ИЛИ с немедленным выполнением.

Специализированные инструкции I-Type, которые нам нужно добавить:

  • slli - Сдвиг влево логический на немедленный.
  • srai — Сдвинуть арифметику вправо на немедленную.
  • srli — логический сдвиг вправо на немедленный.

Инструкции U-Type, которые нам нужно добавить:

  • auipc — добавить верхнюю версию на компьютер.
  • lui - Немедленная загрузка верхнего уровня.

Имена, используемые для кодов операций, естественным образом следуют из названий инструкций, приведенных выше (нет необходимости придумывать новые — все они уникальны). Когда дело доходит до определения семантических функций, помните, что объекты инструкций кодируют интерфейсы исходных операндов, которые не зависят от базового типа операнда. Это означает, что инструкции, которые выполняют одну и ту же операцию, но могут различаться типами операндов, могут использовать одну и ту же семантическую функцию. Например, инструкция addi выполняет ту же операцию, что и инструкция add , если игнорировать тип операнда, поэтому они могут использовать одну и ту же спецификацию семантической функции "&RV32IAdd" . Аналогично для andi , ori , xori и slli . Другие инструкции используют новые семантические функции, но их имена должны быть основаны на операции, а не на операндах, поэтому для srai используйте "&RV32ISra" . Инструкции U-Type auipc и lui не имеют эквивалентов в регистрах, поэтому можно использовать "&RV32IAuipc" и "&RV32ILui" .

Строки дизассемблирования очень похожи на строки в предыдущем упражнении, но, как и следовало ожидать, ссылки на %rs2 заменяются на %imm12 , %uimm5 или %uimm20 , в зависимости от ситуации.

Идите вперед, вносите изменения и стройте. Проверьте сгенерированный вывод. Как и раньше, вы можете проверить свою работу по riscv32i.isa и rv32i_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
имм имм rs2 rs1 функция3 имм имм код операции

Различные непосредственные поля объединяются в 12-битное непосредственное значение со знаком. Поскольку формат на самом деле не имеет значения, мы назовем этот непосредственный bimm12 , что означает «непосредственный 12-битный переход». Фрагментация будет рассмотрена в следующем уроке по созданию двоичного декодера. Все инструкции ветвления сравнивают целочисленные регистры, указанные rs1 и rs2. Если условие истинно, немедленное значение добавляется к текущему значению pc, чтобы получить адрес следующей инструкции, которая будет выполнена. Поэтому операндами для инструкций ветвления должны быть { : rs1, rs2, bimm12 : next_pc } .

Инструкции ветвления, которые нам нужно добавить:

  • beq — Разветвление, если оно равно.
  • bge — Разветвление, если оно больше или равно.
  • bgeu — переход, если он больше или равен беззнаковому.
  • blt — Разветвление, если меньше.
  • bltu — переход, если число меньше беззнакового.
  • bne — Разветвление, если не равно.

Все эти имена кодов операций уникальны, поэтому их можно повторно использовать в описании .isa . Разумеется, необходимо добавить новые имена семантических функций, например, "&RV32IBeq" и ​​т. д.

Спецификация дизассемблирования теперь немного сложнее, поскольку адрес инструкции используется для вычисления пункта назначения, но фактически не является частью операндов инструкции. Однако это часть информации, хранящейся в объекте инструкции, поэтому она доступна. Решение состоит в том, чтобы использовать синтаксис выражения в строке дизассемблирования. Вместо использования «%», за которым следует имя операнда, вы можете ввести %( выражение : формат печати ). Поддерживаются только очень простые выражения, но одним из них является адрес плюс смещение, где для текущего адреса инструкции используется символ @ . Формат печати аналогичен форматам printf в стиле C, но без начального % . Тогда формат дизассемблирования инструкции beq будет следующим:

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

Необходимо добавить только две инструкции перехода и соединения: jal (переход и соединение) и jalr (косвенное соединение и переход).

Инструкция jal использует кодировку J-Type:

31 30..21 20 19..12 11..7 6..0
1 10 1 8 5 7
имм имм имм имм р-д код операции

Как и в случае с инструкциями ветвления, 20-битная непосредственная команда фрагментирована по нескольким полям, поэтому мы назовем ее jimm20 . На данный момент фрагментация не важна, но она будет рассмотрена в следующем уроке по созданию двоичного декодера. Тогда спецификация операнда становится { : jimm20 : next_pc, rd } . Обратите внимание, что в инструкции указано два операнда-адресата: значение следующего ПК и регистр связи.

Подобно приведенным выше инструкциям ветвления, формат дизассемблирования будет следующим:

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

Косвенный переход и соединение использует формат I-Type с 12-битным непосредственным кодом. Он добавляет немедленное значение с расширенным знаком в целочисленный регистр, указанный rs1 , для получения адреса целевой инструкции. Регистр связи — это целочисленный регистр, заданный параметром rd .

31..20 19..15 14..12 11..7 6..0
12 5 3 5 7
имм12 rs1 функция3 р-д код операции

Если бы вы увидели шаблон, вы бы пришли к выводу, что спецификация операнда для jalr должна быть { : rs1, imm12 : next_pc, rd } , а спецификация дизассемблирования:

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

Внесите изменения, а затем создайте. Проверьте сгенерированный вывод. Как и раньше, вы можете проверить свою работу по riscv32i.isa и rv32i_instructions.h .


Добавить инструкции для магазина

Инструкция магазина очень проста. Все они используют формат S-Type:

31..25 24..20 19..15 14..12 11..7 6..0
7 5 5 3 5 7
имм rs2 rs1 функция3 имм код операции

Как видите, это еще один случай фрагментированного 12-битного немедленного кода, назовем его simm12 . Все инструкции сохранения сохраняют значение целочисленного регистра, указанного rs2, по эффективному адресу в памяти, полученному путем добавления значения целочисленного регистра, указанного rs1, к расширенному по знаку значению 12-битного непосредственного значения. Формат операнда должен быть { : rs1, simm12, rs2 } для всех инструкций сохранения.

Инструкции магазина, которые необходимо реализовать:

  • sb — Сохранить байт.
  • sh — сохранить половину слова.
  • sw — слово сохранения.

Спецификация разборки sb такая же, как и следовало ожидать:

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

Спецификации семантической функции также соответствуют ожиданиям: "&RV32ISb" и т. д.

Внесите изменения, а затем создайте. Проверьте сгенерированный вывод. Как и раньше, вы можете проверить свою работу по riscv32i.isa и rv32i_instructions.h .


Добавить инструкции по загрузке

Инструкции загрузки моделируются немного иначе, чем другие инструкции в симуляторе. Чтобы иметь возможность моделировать случаи, когда задержка загрузки неопределенна, инструкции загрузки разделены на два отдельных действия: 1) эффективное вычисление адреса и доступ к памяти и 2) обратная запись результата. В симуляторе это делается путем разделения смыслового действия нагрузки на две отдельные инструкции: основную и дочернюю . Более того, когда мы указываем операнды, нам нужно указывать их как для основной, так и для дочерней инструкции. Это достигается путем обработки спецификации операнда как списка троек. Синтаксис:

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

Все инструкции загрузки используют формат I-Type, как и многие предыдущие инструкции:

31..20 19..15 14..12 11..7 6..0
12 5 3 5 7
имм12 rs1 функция3 р-д код операции

Спецификация операнда разделяет операнды, необходимые для вычисления адреса и инициирования доступа к памяти из регистра-адресата для загружаемых данных: {( : rs1, imm12 : ), ( : : rd) } .

Поскольку семантическое действие разделено на две инструкции, семантические функции аналогичным образом должны указывать два вызываемых объекта. Для lw (загрузочного слова) это будет написано:

    semfunc: "&RV32ILw", "&RV32ILwChild"

Спецификация разборки более традиционна. Об обучении детей не упоминается. Для lw это должно быть:

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

Инструкции по загрузке, которые необходимо реализовать:

  • lb — Загрузить байт.
  • lbu — Загрузочный байт без знака.
  • lh - Загрузить половину слова.
  • lhu — загрузить полуслово без знака.
  • lw — Загрузить слово.

Внесите изменения, а затем создайте. Проверьте сгенерированный вывод. Как и раньше, вы можете проверить свою работу по riscv32i.isa и rv32i_instructions.h .

Спасибо, что зашли так далеко. Мы надеемся, что это было полезно.