Интегрированный декодер RiscV

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

  • Узнайте, как сгенерированные ISA и двоичные декодеры сочетаются друг с другом.
  • Напишите необходимый код C++ для создания полного декодера инструкций для RiscV RV32I, сочетающего в себе ISA и двоичные декодеры.

Понимание декодера инструкций

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

Декодер верхнего уровня реализует generic::DecoderInterface , показанный ниже:

// This is the simulator's interface to the instruction decoder.
class DecoderInterface {
 public:
  // Return a decoded instruction for the given address. If there are errors
  // in the instruciton decoding, the decoder should still produce an
  // instruction that can be executed, but its semantic action function should
  // set an error condition in the simulation when executed.
  virtual Instruction *DecodeInstruction(uint64_t address) = 0;
  virtual ~DecoderInterface() = default;
};

Как видите, необходимо реализовать только один метод: cpp virtual Instruction *DecodeInstruction(uint64_t address);

Теперь давайте посмотрим, что предоставляет и что нужно сгенерированному коду.

Сначала рассмотрим класс верхнего уровня RiscV32IInstructionSet в файле riscv32i_decoder.h , который был создан в конце руководства по декодеру ISA. Чтобы просмотреть содержимое заново, перейдите в каталог решения этого руководства и пересоберите все.

$ cd riscv_isa_decoder/solution
$ bazel build :all
...<snip>...

Теперь измените каталог обратно на корень репозитория, а затем давайте посмотрим на сгенерированные исходные коды. Для этого измените каталог на 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_decoder.h . Есть три класса, на которые нам нужно обратить внимание:

  • RiscV32IEncodingBase
  • RiscV32IInstructionSetFactory
  • RiscV32IInstructionSet

Обратите внимание на названия классов. Все классы названы на основе версии имени в Pascal, указанной в объявлении «isa» в этом файле: isa RiscV32I { ... }

Начнем с класса RiscVIInstructionSet . Это показано ниже:

class RiscV32IInstructionSet {
 public:
  RiscV32IInstructionSet(ArchState *arch_state,
                         RiscV32IInstructionSetFactory *factory);
  Instruction *Decode(uint64 address, RiscV32IEncodingBase *encoding);

 private:
  std::unique_ptr<Riscv32Slot> riscv32_decoder_;
  ArchState *arch_state_;
};

В этом классе нет виртуальных методов, поэтому это отдельный класс, но обратите внимание на две вещи. Сначала конструктор принимает указатель на экземпляр класса RiscV32IInstructionSetFactory . Это класс, который сгенерированный декодер использует для создания экземпляра класса RiscV32Slot , который используется для декодирования всех инструкций, определенных для slot RiscV32 , как определено в файле riscv32i.isa . Во-вторых, метод Decode принимает дополнительный параметр указателя типа на RiscV32IEncodingBase . Это класс, который будет обеспечивать интерфейс между декодером isa, созданным в первом руководстве, и двоичным декодером, созданным во второй лабораторной работе.

Класс RiscV32IInstructionSetFactory — это абстрактный класс, из которого нам нужно создать собственную реализацию полного декодера. В большинстве случаев этот класс тривиален: достаточно предоставить метод для вызова конструктора для каждого класса слота, определенного в нашем файле .isa . В нашем случае все очень просто, поскольку существует только один такой класс: Riscv32Slot (Pascal-регистр имени riscv32 объединенного со Slot ). Метод не создается для вас, поскольку в некоторых расширенных случаях использования может быть полезно получить подкласс из слота и вместо этого вызвать его конструктор.

Последний класс RiscV32IEncodingBase мы рассмотрим позже в этом руководстве, поскольку это тема другого упражнения.


Определить декодер инструкций верхнего уровня

Определить класс фабрики

Если вы пересобирали проект для первого урока, обязательно вернитесь в каталог riscv_full_decoder .

Откройте файл riscv32_decoder.h . Все необходимые включаемые файлы уже добавлены и пространства имен настроены.

После комментария с пометкой //Exercise 1 - step 1 определите класс RiscV32IsaFactory унаследованный от RiscV32IInstructionSetFactory .

class RiscV32IsaFactory : public RiscV32InstructionSetFactory {};

Затем определите переопределение для CreateRiscv32Slot . Поскольку мы не используем какие-либо производные классы Riscv32Slot , мы просто выделяем новый экземпляр, используя std::make_unique .

std::unique_ptr<Riscv32Slot> CreateRiscv32Slot(ArchState *) override {
  return std::make_unique<Riscv32Slot>(state);
}

Если вам нужна помощь (или вы хотите проверить свою работу), полный ответ здесь .

Определить класс декодера

Конструкторы, деструкторы и объявления методов.

Далее пришло время определить класс декодера. В том же файле, что и выше, перейдите к объявлению RiscV32Decoder . Разверните объявление в определение класса, где RiscV32Decoder наследует от generic::DecoderInterface .

class RiscV32Decoder : public generic::DecoderInterface {
  public:
};

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

OpcodeEnum DecodeRiscVInst32(uint32_t inst_word);

Эта функция принимает командное слово, которое необходимо декодировать, и возвращает код операции, соответствующий этой инструкции. С другой стороны, класс DecodeInterface , который реализует RiscV32Decoder передает только адрес. Таким образом, класс RiscV32Decoder должен иметь возможность доступа к памяти для чтения командного слова для передачи в DecodeRiscVInst32() . В этом проекте доступ к памяти осуществляется через простой интерфейс памяти, определенный в .../mpact/sim/util/memory с метким названием util::MemoryInterface , как показано ниже:

  // Load data from address into the DataBuffer, then schedule the Instruction
  // inst (if not nullptr) to be executed (using the function delay line) with
  // context. The size of the data access is based on size of the data buffer.
  virtual void Load(uint64_t address, DataBuffer *db, Instruction *inst,
                    ReferenceCount *context) = 0;

Кроме того, нам нужно иметь возможность передавать экземпляр класса state конструкторам других классов декодера. Соответствующим классом состояния является класс riscv::RiscVState , который является производным от generic::ArchState с добавленной функциональностью для RiscV. Это означает, что мы должны объявить конструктор так, чтобы он мог принимать указатель на state и memory :

RiscV32Decoder(riscv::RiscVState *state, util::MemoryInterface *memory);

Удалите конструктор по умолчанию и переопределите деструктор:

RiscV32Decoder() = delete;
~RiscV32Decoder() override;

Затем объявите метод DecodeInstruction , который нам нужно переопределить из generic::DecoderInterface .

generic::Instruction *DecodeInstruction(uint64_t address) override;

Если вам нужна помощь (или вы хотите проверить свою работу), полный ответ здесь .


Определения элементов данных

Классу RiscV32Decoder потребуются частные члены данных для хранения параметров конструктора и указателя на фабричный класс.

 private:
  riscv::RiscVState *state_;
  util::MemoryInterface *memory_;

Ему также нужен указатель на класс кодирования, производный от RiscV32IEncodingBase , назовем его RiscV32IEncoding (мы реализуем это в упражнении 2). Кроме того, ему нужен указатель на экземпляр RiscV32IInstructionSet , поэтому добавьте:

  RiscV32IsaFactory *riscv_isa_factory_;
  RiscV32IEncoding *riscv_encoding_;
  RiscV32IInstructionSet *riscv_isa_;

Наконец, нам нужно определить элемент данных для использования с нашим интерфейсом памяти:

  generic::DataBuffer *inst_db_;

Если вам нужна помощь (или вы хотите проверить свою работу), полный ответ здесь .

Определите методы класса декодера

Далее пришло время реализовать конструктор, деструктор и метод DecodeInstruction . Откройте файл riscv32_decoder.cc . Пустые методы уже находятся в файле, а также объявления пространства имен и пара объявлений using .

Определение конструктора

Конструктору необходимо только инициализировать элементы данных. Сначала инициализируйте state_ и memory_ :

RiscV32Decoder::RiscV32Decoder(riscv::RiscVState *state,
                               util::MemoryInterface *memory)
    : state_(state), memory_(memory) {

Затем выделите экземпляры каждого из классов, связанных с декодером, передав соответствующие параметры.

  // Allocate the isa factory class, the top level isa decoder instance, and
  // the encoding parser.
  riscv_isa_factory_ = new RiscV32IsaFactory();
  riscv_isa_ = new RiscV32IInstructionSet(state, riscv_isa_factory_);
  riscv_encoding_ = new RiscV32IEncoding(state);

Наконец, выделите экземпляр DataBuffer . Он выделяется с использованием фабрики, доступной через член state_ . Мы выделяем буфер данных размером для хранения одного uint32_t , так как это размер командного слова.

  inst_db_ = state_->db_factory()->Allocate<uint32_t>(1);

Определение деструктора

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

RiscV32Decoder::~RiscV32Decoder() {
  inst_db_->DecRef();
  delete riscv_isa_;
  delete riscv_isa_factory_;
  delete riscv_encoding_;
}

Определение метода

В нашем случае реализация этого метода довольно проста. Мы предполагаем, что адрес выровнен правильно и дополнительная проверка ошибок не требуется.

Во-первых, командное слово должно быть извлечено из памяти с помощью интерфейса памяти и экземпляра DataBuffer .

  memory_->Load(address, inst_db_, nullptr, nullptr);
  uint32_t iword = inst_db_->Get<uint32_t>(0);

Затем мы вызываем экземпляр RiscVIEncoding для анализа командного слова, что необходимо сделать перед вызовом самого декодера ISA. Напомним, что декодер ISA напрямую обращается к экземпляру RiscVIEncoding , чтобы получить код операции и операнды, указанные в командном слове. Мы еще не реализовали этот класс, но давайте используем в качестве этого метода void ParseInstruction(uint32_t) .

  riscv_encoding_->ParseInstruction(iword);

Наконец, мы вызываем декодер ISA, передавая адрес и класс кодирования.

  auto *instruction = riscv_isa_->Decode(address, riscv_encoding_);
  return instruction;

Если вам нужна помощь (или вы хотите проверить свою работу), полный ответ здесь .


Класс кодирования

Класс кодирования реализует интерфейс, который используется классом декодера для получения кода операции инструкции, ее операндов источника и назначения, а также операндов ресурсов. Все эти объекты зависят от информации из декодера двоичного формата, такой как код операции, значения определенных полей в командном слове и т. д. Он отделен от класса декодера, чтобы сохранить его независимость от кодирования и обеспечить поддержку нескольких различных схем кодирования в будущем. .

RiscV32IEncodingBase — это абстрактный класс. Ниже показан набор методов, которые мы должны реализовать в нашем производном классе.

class RiscV32IEncodingBase {
 public:
  virtual ~RiscV32IEncodingBase() = default;

  virtual OpcodeEnum GetOpcode(SlotEnum slot, int entry) = 0;

  virtual ResourceOperandInterface *
              GetSimpleResourceOperand(SlotEnum slot, int entry, OpcodeEnum opcode,
                                       SimpleResourceVector &resource_vec, int end) = 0;

  virtual ResourceOperandInterface *
              GetComplexResourceOperand(SlotEnum slot, int entry, OpcodeEnum opcode,
                                        ComplexResourceEnum resource_op,
                                        int begin, int end) = 0;

  virtual PredicateOperandInterface *
              GetPredicate(SlotEnum slot, int entry, OpcodeEnum opcode,
                           PredOpEnum pred_op) = 0;

  virtual SourceOperandInterface *
              GetSource(SlotEnum slot, int entry, OpcodeEnum opcode,
                        SourceOpEnum source_op, int source_no) = 0;

  virtual DestinationOperandInterface *
              GetDestination(SlotEnum slot, int entry, OpcodeEnum opcode,
                             DestOpEnum dest_op, int dest_no, int latency) = 0;

  virtual int GetLatency(SlotEnum slot, int entry, OpcodeEnum opcode,
                         DestOpEnum dest_op, int dest_no) = 0;
};

На первый взгляд это выглядит немного сложно, особенно из-за количества параметров, но для простой архитектуры, такой как RiscV, мы фактически игнорируем большинство параметров, поскольку их значения будут подразумеваться.

Давайте рассмотрим каждый из методов по очереди.

OpcodeEnum GetOpcode(SlotEnum slot, int entry);

Метод GetOpcode возвращает член OpcodeEnum для текущей инструкции, идентифицирующий код операции инструкции. Класс OpcodeEnum определен в сгенерированном файле декодера isa riscv32i_enums.h . Метод принимает два параметра, оба из которых для наших целей можно игнорировать. Первым из них является тип слота (класс перечисления, также определенный в riscv32i_enums.h ), который, поскольку RiscV имеет только один слот, имеет только одно возможное значение: SlotEnum::kRiscv32 . Второй — номер экземпляра слота (в случае наличия нескольких экземпляров слота, что может произойти в некоторых архитектурах VLIW).

ResourceOperandInterface *
    GetSimpleResourceOperand(SlotEnum slot, int entry, OpcodeEnum opcode,
                                     SimpleResourceVector &resource_vec, int end)

ResourceOperandInterface *
    GetComplexResourceOperand(SlotEnum slot, int entry, OpcodeEnum opcode,
                                      ComplexResourceEnum resource_op,
                                      int begin, int end);

Следующие два метода используются для моделирования аппаратных ресурсов процессора с целью повышения точности цикла. В наших обучающих упражнениях мы не будем их использовать, поэтому в реализации они будут заглушены, возвращая nullptr .

PredicateOperandInterface *
            GetPredicate(SlotEnum slot, int entry, OpcodeEnum opcode,
                         PredOpEnum pred_op);

SourceOperandInterface *
            GetSource(SlotEnum slot, int entry, OpcodeEnum opcode,
                      SourceOpEnum source_op, int source_no);

DestinationOperandInterface *
            GetDestination(SlotEnum slot, int entry, OpcodeEnum opcode,
                           DestOpEnum dest_op, int dest_no, int latency);

Эти три метода возвращают указатели на объекты операндов, которые используются в семантических функциях инструкций для доступа к значению любого операнда-предиката инструкции, каждого из операндов-источников инструкций и записи новых значений в операнды-адресаты инструкций. Поскольку RiscV не использует предикаты инструкций, этому методу достаточно вернуть nullptr .

Шаблон параметров во всех этих функциях аналогичен. Сначала, как и в случае с GetOpcode передаются слот и запись. Затем код операции для инструкции, для которой должен быть создан операнд. Это используется только в том случае, если разные коды операций должны возвращать разные объекты операндов для одних и тех же типов операндов, что не относится к этому симулятору RiscV.

Далее следует предикат, источник и назначение, запись перечисления операндов, которая идентифицирует операнд, который необходимо создать. Они взяты из трех OpEnums в riscv32i_enums.h , как показано ниже:

  enum class PredOpEnum {
    kNone = 0,
    kPastMaxValue = 1,
  };

  enum class SourceOpEnum {
    kNone = 0,
    kBimm12 = 1,
    kCsr = 2,
    kImm12 = 3,
    kJimm20 = 4,
    kRs1 = 5,
    kRs2 = 6,
    kSimm12 = 7,
    kUimm20 = 8,
    kUimm5 = 9,
    kPastMaxValue = 10,
  };

  enum class DestOpEnum {
    kNone = 0,
    kCsr = 1,
    kNextPc = 2,
    kRd = 3,
    kPastMaxValue = 4,
  };

Если вы снова посмотрите на файл riscv32.isa , вы заметите, что они соответствуют наборам имен операндов источника и назначения, используемых в объявлении каждой инструкции. Использование разных имен операндов для операндов, которые представляют разные битовые поля и типы операндов, упрощает написание класса кодирования, поскольку член перечисления однозначно определяет точный тип возвращаемого операнда, и нет необходимости учитывать значения слота, записи, или параметры кода операции.

Наконец, для операндов источника и назначения передается порядковый номер операнда (опять же, мы можем игнорировать это), а для операнда назначения — задержка (в циклах), которая проходит между моментом выдачи инструкции и Результат назначения доступен для последующих инструкций. В нашем симуляторе эта задержка будет равна 0, что означает, что инструкция сразу записывает результат в регистр.

int GetLatency(SlotEnum slot, int entry, OpcodeEnum opcode,
                         DestOpEnum dest_op, int dest_no);

Последняя функция используется для получения задержки определенного операнда назначения, если он указан как * в файле .isa . Это необычно и не используется в этом симуляторе RiscV, поэтому наша реализация этой функции просто вернет 0.


Определить класс кодировки

Заголовочный файл (.h)

Методы

Откройте файл riscv32i_encoding.h . Все необходимые включаемые файлы уже добавлены и пространства имен настроены. Все добавление кода выполняется после комментария // Exercise 2.

Начнем с определения класса RiscV32IEncoding , который наследуется от сгенерированного интерфейса.

class RiscV32IEncoding : public RiscV32IEncodingBase {
 public:

};

Далее конструктор должен получить указатель на экземпляр состояния, в данном случае указатель на riscv::RiscVState . Следует использовать деструктор по умолчанию.

explicit RiscV32IEncoding(riscv::RiscVState *state);
~RiscV32IEncoding() override = default;

Прежде чем мы добавим все методы интерфейса, давайте добавим метод, вызываемый RiscV32Decoder для анализа инструкции:

void ParseInstruction(uint32_t inst_word);

Далее добавим те методы, которые имеют тривиальные переопределения, оставив имена неиспользуемых параметров:

// Trivial overrides.
ResourceOperandInterface *GetSimpleResourceOperand(SlotEnum, int, OpcodeEnum,
                                                   SimpleResourceVector &,
                                                   int) override {
  return nullptr;
}

ResourceOperandInterface *GetComplexResourceOperand(SlotEnum, int, OpcodeEnum,
                                                    ComplexResourceEnum ,
                                                    int, int) override {
  return nullptr;
}

PredicateOperandInterface *GetPredicate(SlotEnum, int, OpcodeEnum,
                                        PredOpEnum) override {
  return nullptr;
}

int GetLatency(SlotEnum, int, OpcodeEnum, DestOpEnum, int) override { return 0; }

Наконец, добавьте оставшиеся переопределения методов общедоступного интерфейса, но с отложенными реализациями в файле .cc.


OpcodeEnum GetOpcode(SlotEnum, int) override;

SourceOperandInterface *GetSource(SlotEnum , int, OpcodeEnum,
                                  SourceOpEnum source_op, int) override;

DestinationOperandInterface *GetDestination(SlotEnum, int, OpcodeEnum,
                                            DestOpEnum dest_op, int,
                                            int latency) override;

Чтобы упростить реализацию каждого из методов получения операндов, мы создадим два массива вызываемых объектов (объектов функций), индексированных числовым значением членов SourceOpEnum и DestOpEnum соответственно. Таким образом, тела этих методов сводятся к вызову объекта функции для переданного значения перечисления и возврату его возвращаемого значения.

Чтобы организовать инициализацию этих двух массивов, мы определяем два частных метода, которые будут вызываться из конструктора следующим образом:

 private:
  void InitializeSourceOperandGetters();
  void InitializeDestinationOperandGetters();

Члены данных

Требуемые элементы данных следующие:

  • state_ для хранения значения riscv::RiscVState * .
  • inst_word_ типа uint32_t , который содержит значение текущего командного слова.
  • opcode_ для хранения кода операции текущей инструкции, который обновляется методом ParseInstruction . Это имеет тип OpcodeEnum .
  • source_op_getters_ массив для хранения вызываемых объектов , используемых для получения объектов исходного операнда. Тип элементов массива — absl::AnyInvocable<SourceOperandInterface *>()> .
  • dest_op_getters_ массив для хранения вызываемых объектов , используемых для получения объектов целевого операнда. Тип элементов массива — absl::AnyInvocable<DestinationOperandInterface *>()> .
  • xreg_alias массив имен ABI целочисленных регистров RiscV, например «ноль» и «ra» вместо «x0» и «x1».

  riscv::RiscVState *state_;
  uint32_t inst_word_;
  OpcodeEnum opcode_;

  absl::AnyInvocable<SourceOperandInterface *()>
      source_op_getters_[static_cast<int>(SourceOpEnum::kPastMaxValue)];
  absl::AnyInvocable<DestinationOperandInterface *(int)>
      dest_op_getters_[static_cast<int>(DestOpEnum::kPastMaxValue)];

  const std::string xreg_alias_[32] = {
      "zero", "ra", "sp", "gp", "tp",  "t0",  "t1", "t2", "s0", "s1", "a0",
      "a1",   "a2", "a3", "a4", "a5",  "a6",  "a7", "s2", "s3", "s4", "s5",
      "s6",   "s7", "s8", "s9", "s10", "s11", "t3", "t4", "t5", "t6"};

Если вам нужна помощь (или вы хотите проверить свою работу), полный ответ здесь .

Исходный файл (.cc).

Откройте файл riscv32i_encoding.cc . Все необходимые включаемые файлы уже добавлены и пространства имен настроены. Все добавление кода выполняется после комментария // Exercise 2.

Вспомогательные функции

Мы начнем с написания пары вспомогательных функций, которые будем использовать для создания операндов регистров источника и назначения. Они будут созданы по шаблону типа регистра и будут вызывать объект RiscVState , чтобы получить дескриптор объекта регистра, а затем вызывать метод фабрики операндов в объекте регистра.

Начнем с помощников операндов назначения:

template <typename RegType>
inline DestinationOperandInterface *GetRegisterDestinationOp(
    RiscVState *state, const std::string &name, int latency) {
  auto *reg = state->GetRegister<RegType>(name).first;
  return reg->CreateDestinationOperand(latency);
}

template <typename RegType>
inline DestinationOperandInterface *GetRegisterDestinationOp(
    RiscVState *state, const std::string &name, int latency,
    const std::string &op_name) {
  auto *reg = state->GetRegister<RegType>(name).first;
  return reg->CreateDestinationOperand(latency, op_name);
}

Как видите, есть две вспомогательные функции. Второй принимает дополнительный параметр op_name , который позволяет операнду иметь другое имя или строковое представление, чем базовый регистр.

Аналогично для помощников исходного операнда:

template <typename RegType>
inline SourceOperandInterface *GetRegisterSourceOp(RiscVState *state,
                                                   const std::string &reg_name) {
  auto *reg = state->GetRegister<RegType>(reg_name).first;
  auto *op = reg->CreateSourceOperand();
  return op;
}

template <typename RegType>
inline SourceOperandInterface *GetRegisterSourceOp(RiscVState *state,
                                                   const std::string &reg_name,
                                                   const std::string &op_name) {
  auto *reg = state->GetRegister<RegType>(reg_name).first;
  auto *op = reg->CreateSourceOperand(op_name);
  return op;
}

Конструктор и интерфейсные функции

Конструктор и функции интерфейса очень просты. Конструктор просто вызывает два метода инициализации для инициализации массивов вызываемых объектов для геттеров операндов.

RiscV32IEncoding::RiscV32IEncoding(RiscVState *state) : state_(state) {
  InitializeSourceOperandGetters();
  InitializeDestinationOperandGetters();
}

ParseInstruction сохраняет командное слово, а затем код операции, полученный в результате вызова кода, сгенерированного двоичным декодером.

// Parse the instruction word to determine the opcode.
void RiscV32IEncoding::ParseInstruction(uint32_t inst_word) {
  inst_word_ = inst_word;
  opcode_ = mpact::sim::codelab::DecodeRiscVInst32(inst_word_);
}

Наконец, методы получения операндов возвращают значение из вызываемой им функции получения на основе поиска в массиве с использованием значения перечисления операнда назначения/источника.


DestinationOperandInterface *RiscV32IEncoding::GetDestination(
    SlotEnum, int, OpcodeEnum, DestOpEnum dest_op, int, int latency) {
  return dest_op_getters_[static_cast<int>(dest_op)](latency);
}

SourceOperandInterface *RiscV32IEncoding::GetSource(SlotEnum, int, OpcodeEnum,
                                                    SourceOpEnum source_op, int) {
  return source_op_getters_[static_cast<int>(source_op)]();
}

Методы инициализации массива

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

Вызовите сгенерированный класс DestOpEnum из riscv32i_enums.h :

  enum class DestOpEnum {
    kNone = 0,
    kCsr = 1,
    kNextPc = 2,
    kRd = 3,
    kPastMaxValue = 4,
  };

Для dest_op_getters_ нам нужно инициализировать 4 записи, по одной для kNone , kCsr , kNextPc и kRd . Для удобства каждая запись инициализируется с помощью лямбды, хотя вы также можете использовать любую другую форму вызываемого объекта. Подпись лямбды — void(int latency) .

До сих пор мы мало говорили о различных типах операндов-адресатов, определенных в MPACT-Sim. В этом упражнении мы будем использовать только два типа: generic::RegisterDestinationOperand определенный в register.h , и generic::DevNullOperand определенный в devnull_operand.h . Детали этих операндов сейчас не очень важны, за исключением того, что первый используется для записи в регистры, а второй игнорирует все записи.

Первая запись для kNone тривиальна — просто верните nullptr и, при необходимости, зарегистрируйте ошибку.

void RiscV32IEncoding::InitializeDestinationOperandGetters() {
  // Destination operand getters.
  dest_op_getters_[static_cast<int>(DestOpEnum::kNone)] = [](int) {
    return nullptr;
  };

Далее идет kCsr . Здесь мы немного схитрим . Программа «Hello World» не опирается на какое-либо фактическое обновление CSR, но существует некоторый шаблонный код, выполняющий инструкции CSR. Решение состоит в том, чтобы просто создать имитацию этого, используя обычный регистр с именем «CSR» и направлять все такие записи в него.

  dest_op_getters_[static_cast<int>(DestOpEnum::kCsr)] = [this](int latency) {
    return GetRegisterDestinationOp<RV32Register>(state_, "CSR", latency);
  };

Далее идет kNextPc , который относится к регистру «pc». Он используется в качестве цели для всех инструкций ветвления и перехода. Имя определяется в RiscVState как kPcName .

  dest_op_getters_[static_cast<int>(DestOpEnum::kNextPc)] = [this](int latency) {
    return GetRegisterDestinationOp<RV32Register>(state_, RiscVState::kPcName, latency);
  }

Наконец, есть операнд назначения kRd . В riscv32i.isa операнд rd используется только для ссылки на целочисленный регистр, закодированный в поле «rd» командного слова, поэтому нет никакой двусмысленности, к которой он относится. Есть только одна сложность. Регистр x0 (abi name zero ) жестко привязан к 0, поэтому для этого регистра мы используем DevNullOperand .

Итак, в этом геттере мы сначала извлекаем значение из поля rd , используя метод Extract , сгенерированный из файла .bin_fmt. Если значение равно 0, мы возвращаем операнд «DevNull», в противном случае мы возвращаем правильный операнд регистра, стараясь использовать соответствующий псевдоним регистра в качестве имени операнда.

  dest_op_getters_[static_cast<int>(DestOpEnum::kRd)] = [this](int latency) {
    // First extract register number from rd field.
    int num = inst32_format::ExtractRd(inst_word_);
    // For register x0, return the DevNull operand.
    if (num == 0) return new DevNullOperand<uint32_t>(state, {1});
    // Return the proper register operand.
    return GetRegisterDestinationOp<RV32Register>(
      state_, absl::StrCat(RiscVState::kXRegPrefix, num), latency,
      xreg_alias_[num]);
    )
  }
}

Теперь перейдем к методу InitializeSourceOperandGetters() , шаблон которого практически такой же, но детали немного отличаются.

Сначала давайте взглянем на SourceOpEnum , созданный из riscv32i.isa в первом уроке:

  enum class SourceOpEnum {
    kNone = 0,
    kBimm12 = 1,
    kCsr = 2,
    kImm12 = 3,
    kJimm20 = 4,
    kRs1 = 5,
    kRs2 = 6,
    kSimm12 = 7,
    kUimm20 = 8,
    kUimm5 = 9,
    kPastMaxValue = 10,
  };

Рассматривая членов, помимо kNone , они делятся на две группы. Один из них — непосредственные операнды: kBimm12 , kImm12 , kJimm20 , kSimm12 , kUimm20 и kUimm5 . Остальные — регистровые операнды: kCsr , kRs1 и kRs2 .

Операнд kNone обрабатывается так же, как и операнды назначения — возвращает nullptr.

void RiscV32IEncoding::InitializeSourceOperandGetters() {
  // Source operand getters.
  source_op_getters_[static_cast<int>(SourceOpEnum::kNone)] = [] () {
    return nullptr;
  };

Далее давайте поработаем над регистровыми операндами. Мы будем обрабатывать kCsr аналогично тому, как мы обрабатывали соответствующие операнды назначения — просто вызовите вспомогательную функцию, используя «CSR» в качестве имени регистра.

  // Register operands.
  source_op_getters_[static_cast<int>(SourceOpEnum::kCsr)] = [this]() {
    return GetRegisterSourceOp<RV32Register>(state_, "CSR");
  };

Операнды kRs1 и kRs2 обрабатываются аналогично kRd , за исключением того, что, хотя мы не хотим обновлять x0 (или zero ), мы хотим быть уверены, что мы всегда читаем 0 из этого операнда. Для этого мы будем использовать generic::IntLiteralOperand<> определенный в literal_operand.h . Этот операнд используется для хранения буквального значения (в отличие от смоделированного непосредственного значения). В противном случае шаблон тот же: сначала извлеките значение rs1/rs2 из командного слова, если оно равно нулю, верните литеральный операнд с параметром шаблона 0, в противном случае верните обычный исходный операнд регистра, используя вспомогательную функцию, используя псевдоним abi как имя операнда.

  source_op_getters_[static_cast<int>(SourceOpEnum::kRs1)] =
      [this]() -> SourceOperandInterface * {
    int num = inst32_format::ExtractRs1(inst_word_);
    if (num == 0) return new IntLiteralOperand<0>({1}, xreg_alias_[0]);
    return GetRegisterSourceOp<RV32Register>(
        state_, absl::StrCat(RiscVState::kXregPrefix, num), xreg_alias_[num]);
  };
  source_op_getters_[static_cast<int>(SourceOpEnum::kRs2)] =
      [this]() -> SourceOperandInterface * {
    int num = inst32_format::ExtractRs2(inst_word_);
    if (num == 0) return new IntLiteralOperand<0>({1}, xreg_alias_[0]);
    return GetRegisterSourceOp<RV32Register>(
        state_, absl::StrCat(RiscVState::kXregPrefix, num), xreg_alias_[num]);
  };

Наконец, мы обрабатываем различные непосредственные операнды. Непосредственные значения хранятся в экземплярах класса generic::ImmediateOperand<> определенного в immediate_operand.h . Единственная разница между различными методами получения непосредственных операндов заключается в том, какая функция Extractor используется и является ли тип хранения знаковым или беззнаковым в зависимости от битового поля.

  // Immediates.
  source_op_getters_[static_cast<int>(SourceOpEnum::kBimm12)] = [this]() {
    return new ImmediateOperand<int32_t>(
        inst32_format::ExtractBImm(inst_word_));
  };
  source_op_getters_[static_cast<int>(SourceOpEnum::kImm12)] = [this]() {
    return new ImmediateOperand<int32_t>(
        inst32_format::ExtractImm12(inst_word_));
  };
  source_op_getters_[static_cast<int>(SourceOpEnum::kUimm5)] = [this]() {
    return new ImmediateOperand<uint32_t>(
        inst32_format::ExtractUimm5(inst_word_));
  };
  source_op_getters_[static_cast<int>(SourceOpEnum::kJimm20)] = [this]() {
    return new ImmediateOperand<int32_t>(
        inst32_format::ExtractJImm(inst_word_));
  };
  source_op_getters_[static_cast<int>(SourceOpEnum::kSimm12)] = [this]() {
    return new ImmediateOperand<int32_t>(
        inst32_format::ExtractSImm(inst_word_));
  };
  source_op_getters_[static_cast<int>(SourceOpEnum::kUimm20)] = [this]() {
    return new ImmediateOperand<uint32_t>(
        inst32_format::ExtractUimm32(inst_word_));
  };
}

Если вам нужна помощь (или вы хотите проверить свою работу), полный ответ здесь .

На этом урок завершается. Мы надеемся, что это было полезно.