Decodificador RiscV ISA

Os objetivos deste tutorial são:

  • Saiba como as instruções são representadas no simulador MPACT-Sim.
  • Conheça a estrutura e a sintaxe do arquivo de descrição da ISA.
  • Escrever as descrições da ISA para o subconjunto de instruções do RiscV RV32I

Visão geral

No MPACT-Sim, as instruções de destino são decodificadas e armazenadas em um representação para tornar suas informações mais disponíveis, e a semântica mais rápidos de executar. Essas instâncias são armazenadas em cache para reduzir o número de vezes que instruções executadas com frequência são executada.

A classe de instrução

Antes de começarmos, é útil analisar um pouco como as instruções representada em MPACT-Sim. A classe Instruction é definida em mpact-sim/mpact/sim/generic/instruction.h.

A instância da classe Instruction contém todas as informações necessárias para simular a instrução quando ela é "executada", como:

  1. Endereço das instruções, tamanho da instrução simulada, por exemplo, tamanho em .texto.
  2. Código de operação da instrução.
  3. Ponteiro da interface do operando predicado (se aplicável).
  4. Vetor de ponteiros da interface do operando de origem.
  5. Vetor de ponteiros da interface do operando de destino.
  6. Função semântica chamável.
  7. Ponteiro para o objeto de estado da arquitetura.
  8. Ponteiro para o objeto de contexto.
  9. Ponteiro para instâncias filhas e próximas de instrução.
  10. String de desmontagem.

Essas instâncias geralmente são armazenadas em um cache de instruções (instância). reutilizadas sempre que a instrução for executada novamente. Isso melhora a performance durante o tempo de execução.

Com exceção do ponteiro para o objeto de contexto, todos são preenchidos pelo decodificador de instruções que é gerado a partir da descrição da ISA. Para isso tutorial, não é necessário conhecer os detalhes desses itens, pois não seremos usando-os diretamente. Em vez disso, uma compreensão de alto nível de como eles são usados suficientes.

A função semântica chamável é o objeto de função/método/função C++ (incluindo lambdas) que implementa a semântica da instrução. Para para uma instrução add, ele carrega cada operando de origem, adiciona os dois operandos e grava o resultado em um único operando de destino. O assunto o tutorial das funções semânticas é abordado em detalhes.

Operandos de instrução

A classe de instrução inclui ponteiros para três tipos de interfaces de operandos: predicado, origem e destino. Essas interfaces permitem que as funções semânticas ser escritos independentemente do tipo real da instrução subjacente; operando. Por exemplo, é possível acessar os valores de registros e imediatos na mesma interface. Isso significa que as instruções que executam a mesma operação, mas em diferentes operandos (por exemplo, registros vs. imedates) podem ser implementados usando a mesma função semântica.

A interface de operando do predicado para os ISAs que oferecem suporte aos valores execução de instrução (para outros ISAs é nulo), é usada para determinar se um a instrução deve ser executada com base no valor booleano do predicado.

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

A interface de operando de origem permite que a função semântica de instrução leia valores dos operandos de instruções sem considerar o operando subjacente; não é válido. Os métodos da interface oferecem suporte a operandos com valores escalares e vetoriais.

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

A interface de operandos de destino fornece métodos para alocação e processamento Instâncias de DataBuffer (o tipo de dados interno usado para armazenar valores de registro). Um operando de destino também tem uma latência associada, que é o número de ciclos para aguardar até que a instância do buffer de dados seja alocada pela instrução A função semântica é usada para atualizar o valor do registro de destino. Para exemplo, a latência de uma instrução add pode ser 1, enquanto que para um mpy de instrução, ele pode ser 4. Isso é abordado com mais detalhes no tutorial sobre funções semânticas.

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

Descrição da ISA

A arquitetura do conjunto de instruções (ISA) de um processador define o modelo abstrato pela qual o software interage com o hardware. Ela define o conjunto de instruções disponíveis, os tipos de dados, registros e outros estados de máquina as instruções em operação, bem como seu comportamento (semântica). Para os objetivos do MPACT-Sim, o ISA não inclui a codificação real das instruções. que é tratado separadamente.

O ISA do processador é expresso em um arquivo de descrição que descreve a conjunto de instruções em um nível abstrato e agnóstico de codificação. O arquivo de descrição enumera o conjunto de instruções disponíveis. Para cada instrução, obrigatório listar o nome, o número e os nomes dos operandos e vinculação a uma função/chamável do C++ que implementa a semântica dela. Além disso, é possível especificar uma string de formatação "disassembly" e o uso da instrução nomes de recursos de hardware. O primeiro é útil para produzir um design representação gráfica da instrução para depuração, rastreamento ou uso interativo. A último pode ser usado para aumentar a precisão do ciclo na simulação.

O arquivo de descrição da ISA é analisado pelo analisador isa que gera o código para o decodificador de instruções independente de representação. Esse decodificador é responsável preencher os campos dos objetos de instrução. Os valores específicos, digamos, de registro de destino, são obtidos de uma instrução específica de formato decodificador. Um desses decodificadores é o binário, que é o foco do no próximo tutorial.

Este tutorial mostra como escrever um arquivo de descrição da ISA para um modelo do Terraform. Usaremos um subconjunto do conjunto de instruções RiscV RV32I para ilustrar isso e, junto com os outros tutoriais, criar um simulador capaz de de simular um aplicativo "Hello World" neste programa. Para mais detalhes sobre o ISA do RiscV, acesse Especificações do Risc-V.

Comece abrindo o arquivo: riscv_isa_decoder/riscv32i.isa

O conteúdo do arquivo é dividido em várias seções. O primeiro é o ISA, declaração:

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

Isso declara RiscV32I como o nome da ISA, e o gerador de código crie uma classe chamada RiscV32IEncodingBase que define a interface o decodificador gerado usará para obter as informações do código de operação e do operando. O nome essa classe é gerada convertendo o nome da ISA em Pascal-case, concatenando com EncodingBase. A declaração slots { riscv32; } especifica que há apenas um slot de instrução riscv32 no RiscV32I ISA (em vez de vários slots em uma instrução VLIW) e que o único Instruções válidas são aquelas definidas para execução em riscv32.

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

Isso especifica que o primeiro fragmento de desmontagem de qualquer arquivo (veja mais abaixo), será justificado à esquerda com 15 caracteres campo amplo. Os fragmentos subsequentes serão anexados a este campo sem espaço adicional.

Abaixo disso, há três declarações de slot: riscv32i, zicsr e riscv32. Com base na definição de isa acima, somente as instruções definidas para o riscv32. vai fazer parte da isa RiscV32I. Para que servem os outros dois slots?

É possível usar slots para dividir instruções em grupos separados, combinado em um único slot no final. Observe a notação : riscv32i, zicsr. na declaração de slot riscv32. Isso especifica que o slot riscv32 herda todas as instruções definidas nos slots zicsr e riscv32i. A ISA de 32 bits do RiscV consiste em um ISA básico chamado RV32I, ao qual um conjunto de extensões opcionais pode ser adicionados. O mecanismo de slot permite que as instruções nessas extensões sejam especificados separadamente e combinados no final conforme necessário para definir os da ISA como um todo. Nesse caso, as instruções no campo "I" do RiscV do grupo de anúncios estejam definidos separadamente daqueles no "zicsr" grupo. É possível definir outros grupos para "M" (multiplicar/dividir), 'F' (ponto flutuante de precisão única), 'D' (ponto flutuante de precisão dupla), 'C' (instruções compactas de 16 bits) etc. como necessário para a ISA final do RiscV.

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

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

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

As definições de slot zicsr e riscv32 não precisam ser alteradas. No entanto, o foco deste tutorial é adicionar as definições necessárias ao riscv32i espaço. Vamos analisar melhor o que está definido atualmente nesse slot:

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

Primeiro, há uma seção includes {} que lista os arquivos principais que precisam seja incluído no código gerado quando este slot for referenciado, diretamente ou indiretamente, no último ISA. Os arquivos incluídos também podem ser listados em formato seção includes {} com escopo. Nesse caso, elas serão sempre incluídas. Isso pode será útil se o mesmo arquivo de inclusão tiver que ser adicionado a todos os espaços definição.

As declarações default size e default latency definem que, a menos que especificado de outra forma, o tamanho de uma instrução é 4, e a latência de uma gravação do operando de destino é de 0 ciclos. Observe que o tamanho da instrução especificada aqui, é o tamanho do incremento do contador do programa para calcular o endereço da próxima instrução sequencial a ser executada na simulação na nuvem. Pode ou não ser igual ao tamanho em bytes do representação de instrução no arquivo executável de entrada.

A seção de código de operação está no centro da definição de slots. Como você pode ver, apenas dois os códigos de operação (instruções) fence e ebreak foram definidos até agora em riscv32i O código de operação fence é definido especificando o nome (fence) e a especificação do operando ({: imm12 : }), seguida do código de operação opcional ("fence") e a função chamável que será vinculada como a semântica ("&RV32IFence").

Os operandos de instrução são especificados como um triplo, com cada componente separados por ponto e vírgula, predicate ':' lista de operandos de origem ':' lista de operandos de destino. As listas de operandos de origem e destino são vírgulas listas separadas de nomes de operandos. Como você pode ver, os operandos de instrução para a instrução fence contém, nenhum operando de predicado, apenas uma origem. nome do operando imm12 e nenhum operando de destino. O subconjunto RiscV RV32I faz não oferece suporte à execução predicada, então o operando de predicado estará sempre vazio neste tutorial.

A função semântica é especificada como a string necessária para especificar o objeto C++ ou chamável a ser usada para chamar a função semântica. A assinatura do função semântica/chamável é void(Instruction *).

A especificação desmontagem consiste em uma lista de strings separadas por vírgulas. Normalmente, são usadas apenas duas strings, uma para o código de operação e outra para o operandos. Quando formatados (usando a chamada AsString() na instrução), cada string é formatada dentro de um campo de acordo com a disasm widths especificação descrita acima.

Os exercícios a seguir ajudam você a adicionar instruções ao arquivo riscv32i.isa suficiente para simular um aplicativo de "Hello World" neste programa. Para quem está com pressa, soluções podem ser encontradas riscv32i.isa e rv32i_instructions.h.


Executar o build inicial

Se você não mudou o diretório para riscv_isa_decoder, faça isso agora. Depois, crie o projeto da seguinte maneira: ela deverá ser bem-sucedida.

$ cd riscv_isa_decoder
$ bazel build :all

Agora mude seu diretório de volta para a raiz do repositório, depois vamos conferir nas origens geradas. Para isso, mude o diretório para bazel-out/k8-fastbuild/bin/riscv_isa_decoder (supondo que você esteja em uma arquitetura x86) host: para outros hosts, o k8-fastbuild será outra string).

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

Nesse diretório, entre outros arquivos, haverá o seguinte arquivos C++ gerados:

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

Para conferir o riscv32i_enums.h, clique nele no navegador. Você deve ele contém algo como:

#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

Como você pode ver, cada slot, código de operação e operando que foi definido no O arquivo riscv32i.isa é definido em um dos tipos de enumeração. Além disso, há uma matriz OpcodeNames que armazena todos os nomes dos códigos de operação (ele é definido em riscv32i_enums.cc). Os outros arquivos contêm o decodificador gerado, que será abordado em outro tutorial.

Regra de build do Bazel

O destino do decodificador de ISA no Bazel é definido usando uma macro de regra personalizada chamada mpact_isa_decoder, que é carregado de mpact/sim/decoder/mpact_sim_isa.bzl. no repositório mpact-sim. Para este tutorial, o destino do build definido em riscv_isa_decoder/BUILD é:

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

Essa regra chama a ferramenta de análise e o gerador da ISA para gerar o código C++, e compila o gerado em uma biblioteca da qual outras regras podem depender usando o rótulo //riscv_isa_decoder:riscv32i_isa. A seção includes é usada para especificar outros arquivos .isa que o arquivo de origem pode incluir. A isa_name é usado para especificar qual isa específica, obrigatório se mais de um for especificado, no arquivo de origem para o qual gerar o decodificador.


Adicionar instruções de ALU de registro/registro

Agora é hora de adicionar algumas novas instruções ao arquivo riscv32i.isa. A primeira O grupo de instruções são instruções de ALU de registro, como add, and etc. No RiscV32, todos eles usam o formato de instrução binário do tipo R:

31 a 25 24 a 20 19 a 15 14 a 12 11 a 7 6 a 0
7 5 5 3 5 7
func7 rs2 rs1 func3 a código de operação

Embora o arquivo .isa seja usado para gerar um decodificador independente de formato, ele ainda é útil considerar o formato binário e seu layout para orientar as entradas. Conforme você verá que há três campos relevantes para o decodificador que preenche as objetos de instrução: rs2, rs1 e rd. Neste ponto, vamos usar esses nomes para registros inteiros que são codificados da mesma maneira (sequências de bits), nos mesmos campos de instrução, em todas as instruções.

As instruções que vamos adicionar são as seguintes:

  • add: adição de número inteiro.
  • and: bit a bit e.
  • or: bit a bit ou.
  • sll: desloca para a esquerda lógico.
  • sltu: definido como menor que, sem assinatura.
  • sub: subtração de números inteiros.
  • xor: xor bit a bit.

Cada uma dessas instruções será adicionada à seção opcodes do riscv32i. Lembre-se de que precisamos especificar o nome, os códigos de operação disassembly e função semântica para cada instrução. O nome é fácil, vamos usar os nomes dos códigos de operação acima. Além disso, todas elas usam os mesmos operandos, portanto, podemos usar { : rs1, rs2 : rd} para a especificação do operando. Isso significa que o operando de origem de registro especificado por rs1 terá índice 0 na origem vetor de operando no objeto de instrução, o operando de origem de registro especificado por rs2 terá índice 1, e o operando de registro de destino especificado por rd será o único elemento no vetor de operando de destino (no índice 0).

A seguir, a especificação da função semântica. Isso é feito usando a palavra-chave semfunc e uma string C++ que especifica um elemento chamável que pode ser usado para atribuir para um std::function. Neste tutorial, usaremos as funções, de modo que a função chamável string será "&MyFunctionName". Usando o esquema de nomenclatura sugerido pelo fence, que precisam ser "&RV32IAdd", "&RV32IAnd" etc.

Por último, a especificação de desmontagem. Começa com a palavra-chave disasm e é seguido por uma lista de strings separadas por vírgulas que especifica como o deve ser impressa como uma string. Usar um sinal % na frente de um O nome do operando indica uma substituição de string usando a representação de string de esse operando. Para a instrução add, isso seria: disasm: "add", "%rd, %rs1,%rs2". Isso significa que a entrada da instrução add será parecida com como:

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

Edite o arquivo riscv32i.isa e adicione todas estas instruções ao .isa descrição. Se você precisar de ajuda (ou quiser verificar seu trabalho), o arquivo de descrição é aqui.

Depois que as instruções forem adicionadas ao arquivo riscv32i.isa, faça o seguinte: para adicionar declarações de função para cada uma das novas funções semânticas que foram referenciada para o arquivo rv32i_instructions.h localizado em "../semantic_functions/". Mais uma vez, se você precisar de ajuda (ou quiser verificar seu trabalho), A resposta é: aqui.

Depois de fazer isso, volte para o riscv_isa_decoder e recrie-o. Fique à vontade para examinar os arquivos de origem gerados.


Adicionar instruções de ALU com ações imediatas

O próximo conjunto de instruções que adicionaremos são instruções de ALU que usam uma valor imediato em vez de um dos registros. Há três grupos instruções (com base no campo imediato): as instruções imediatas I-Type com uma assinatura imediata de 12 bits, as instruções imediatas especializadas do tipo I-Type para shifts, e o U-Type imediato, com um valor imediato não assinado de 20 bits. Os formatos são mostrados abaixo:

O formato imediato do tipo I:

31 a 20 19 a 15 14 a 12 11 a 7 6 a 0
12 5 3 5 7
imm12 rs1 func3 a código de operação

O formato imediato especializado I-Type:

31 a 25 24 a 20 19 a 15 14 a 12 11 a 7 6 a 0
7 5 5 3 5 7
func7 uimm5 rs1 func3 a código de operação

O formato imediato do tipo U:

31 a 12 11 a 7 6 a 0
20 5 7
uimm20 a código de operação

Como você pode ver, os nomes de operandos rs1 e rd se referem aos mesmos campos de bit que anteriormente e são usados para representar registros de números inteiros. Portanto, esses nomes podem ser mantidos. Os campos de valor imediatos têm tamanho e local diferentes, e dois (uimm5 e uimm20) não estão assinados, enquanto imm12 está assinado. Cada um de que usarão seus próprios nomes.

Os operandos para as instruções I-Type precisam ser { : rs1, imm12 :rd }. Para as instruções especializadas do I-Type, ele precisa ser { : rs1, uimm5 : rd}. A especificação do operando de instrução do tipo U-Type precisa ser { : uimm20 : rd }.

As instruções I-Type que precisamos adicionar são:

  • addi: adicionar imediatamente.
  • andi: bit a bit e com imediato.
  • ori: bit a bit ou com imediato.
  • xori: xor bit a bit com imediato.

As instruções I-Type especializadas que precisamos adicionar são:

  • slli: desloca a lógica para a esquerda imediatamente.
  • srai: desloca a aritmética da direita por imediato.
  • srli: desloca a lógica à direita imediatamente.

As instruções do tipo u que precisamos adicionar são:

  • auipc: adicionar o imediatamente superior ao PC.
  • lui: carregar imediatamente superior.

Os nomes a serem usados para os códigos de operação seguem naturalmente os nomes das instruções acima (não é necessário criar novos, todos são únicos). Quando se trata de especificar as funções semânticas, lembre-se de que os objetos de instrução codificam interfaces para os operandos de origem que são agnósticos em relação ao operando subjacente não é válido. Isso significa que, para instruções que têm a mesma operação, mas podem diferir em tipos de operandos, podem compartilhar a mesma função semântica. Por exemplo: a instrução addi executa a mesma operação que a instrução add se um ignora o tipo de operando, então eles podem usar a mesma função semântica especificação "&RV32IAdd". O mesmo acontece com andi, ori, xori e slli. As outras instruções usam novas funções semânticas, mas precisam ser nomeadas. com base na operação, e não em operandos. Portanto, para srai, use "&RV32ISra". A As instruções do tipo U auipc e lui não têm registros equivalentes. Portanto, tudo bem para usar "&RV32IAuipc" e "&RV32ILui".

As strings de desmontagem são muito semelhantes às do exercício anterior, mas como esperado, as referências a %rs2 são substituídas por %imm12, %uimm5 ou %uimm20, conforme apropriado.

Vá em frente, faça as alterações e crie. Verifique a saída gerada. Assim como antes, você pode comparar seu trabalho riscv32i.isa e o rv32i_instructions.h.


As instruções de ramificação e jump-and-link que precisamos adicionar usam um destino operando que está implícito apenas na própria instrução, ou seja, o próximo operador . Nesta fase, vamos tratar isso como um operando adequado com o nome next_pc: Isso será definido em um tutorial futuro.

Instruções da ramificação

As ramificações que estamos adicionando usam a codificação B-Type.

31 30 a 25 24 a 20 19 a 15 14 a 12 11 a 8 7 6 a 0
1 6 5 5 3 4 1 7
imm imm rs2 rs1 func3 imm imm código de operação

Os diferentes campos imediatos são concatenados em uma mensagem imediata assinada de 12 bits . Como o formato não é realmente relevante, chamaremos isso de imediato bimm12, para ramificação imediata de 12 bits. A fragmentação será abordada em o próximo tutorial sobre como criar o decodificador binário. Todos os as instruções da ramificação comparam os registros inteiros especificados por rs1 e rs2, se a condição for verdadeira, o valor imediato é adicionado ao valor de PC atual para produz o endereço da próxima instrução a ser executada. Os operandos para o Portanto, as instruções da ramificação precisam ser { : rs1, rs2, bimm12 : next_pc }.

As instruções da ramificação que precisamos adicionar são:

  • beq: ramificação, se igual.
  • bge: ramificação, se maior ou igual.
  • bgeu: ramificação se for maior ou igual a sem assinatura.
  • blt: ramificação, se for menor que.
  • bltu: ramificação se for menor do que sem assinatura.
  • bne: ramifica se não for igual.

Esses nomes de código de operação são exclusivos e podem ser reutilizados no .isa. descrição. Obviamente, novos nomes de função semântica precisam ser adicionados, por exemplo, "&RV32IBeq" etc.

A especificação de disassembly agora é um pouco mais complicada, pois o endereço do é usada para calcular o destino, sem que ele faça parte dos operandos de instrução. No entanto, ele faz parte das informações armazenadas em objeto de instrução, para que ele fique disponível. A solução é usar sintaxe de expressão na string desmontagem. Em vez de usar "%" seguido por o nome do operando, é possível digitar %(expression: print format). Apenas muito simples são aceitas, mas o endereço mais o deslocamento é uma delas, com o @ símbolo usado para o endereço da instrução atual. O formato de impressão é semelhante a formatos printf de estilo C, mas sem o % à esquerda. O formato desmontado para a instrução beq se tornará:

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

Somente duas instruções de jump e link precisam ser adicionadas, jal (jump-and-link) e jalr (pular e link indireto).

A instrução jal usa a codificação J-Type:

31 30 a 21 20 19 a 12 11 a 7 6 a 0
1 10 1 8 5 7
imm imm imm imm a código de operação

Assim como nas instruções da ramificação, o imediato de 20 bits é fragmentado em vários campos, então vamos chamá-lo de jimm20. A fragmentação não é importante no momento, mas isso será abordado nos próximos tutorial sobre como criar o decodificador binário. O operando a especificação se tornará { : jimm20 : next_pc, rd }. Observe que há dois operandos de destino, o próximo valor de PC e o registro de link especificado no instrução.

Assim como nas instruções da ramificação acima, o formato desmontado (disassembly) se torna:

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

O jump-and-link indireto usa o formato I-Type com o imediato de 12 bits. Ela adiciona o valor imediato estendido de sinal ao registro inteiro especificado por rs1 para produzir o endereço da instrução de destino. O registro de links é o registro de número inteiro especificado por rd.

31 a 20 19 a 15 14 a 12 11 a 7 6 a 0
12 5 3 5 7
imm12 rs1 func3 a código de operação

Se você viu o padrão, agora deduziria que a especificação do operando de jalr precisa ser { : rs1, imm12 : next_pc, rd }, e o código de desmontagem especificação:

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

Vá em frente, faça as alterações e crie. Verifique a saída gerada. Assim como antes, você pode comparar seu trabalho riscv32i.isa e rv32i_instructions.h.


Adicionar instruções da loja

As instruções para a loja são muito simples. Todos eles usam o formato S-Type:

31 a 25 24 a 20 19 a 15 14 a 12 11 a 7 6 a 0
7 5 5 3 5 7
imm rs2 rs1 func3 imm código de operação

Como você pode ver, este é outro caso de um imediato fragmentado de 12 bits. com o nome simm12. Todas as instruções de armazenamento armazenam o valor do número inteiro registro especificado por rs2 ao endereço efetivo na memória obtido pela adição o valor do registro inteiro especificado por rs1 para o valor estendido por sinal de imediato de 12 bits. O formato do operando deve ser { : rs1, simm12, rs2 } para todas as instruções da loja.

Estas são as instruções de loja que precisam ser implementadas:

  • sb: byte do armazenamento.
  • sh: armazena meia palavra.
  • sw: palavra da loja.

A especificação de desmontagem para sb é a esperada:

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

As especificações da função semântica também são o que você espera: "&RV32ISb", etc.

Vá em frente, faça as alterações e crie. Verifique a saída gerada. Assim como antes, você pode comparar seu trabalho riscv32i.isa e rv32i_instructions.h.


Adicionar instruções de carregamento

As instruções de carregamento são modeladas de forma um pouco diferente das outras no simulador. Para modelar casos em que a latência de carregamento é incerto, as instruções de carregamento são divididas em duas ações separadas: 1) efetivas computação de endereço e acesso à memória e 2) write-back do resultado. Na que é feito dividindo a ação semântica da carga em duas Separadas, a instrução principal e uma instrução secundária. Além disso, quando especificamos operandos, precisamos especificá-los para o main e o child. Isso é feito tratando a especificação do operando como um de quiálteras. A sintaxe é:

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

Todas as instruções de carregamento usam o formato I-Type, assim como muitas instruções anteriores instruções:

31 a 20 19 a 15 14 a 12 11 a 7 6 a 0
12 5 3 5 7
imm12 rs1 func3 a código de operação

A especificação de operando divide os operandos necessários para calcular o endereço. e inicie o acesso à memória pelo destino de registro dos dados de carregamento: {( : rs1, imm12 : ), ( : : rd) }:

Como a ação semântica é dividida em duas instruções, as funções semânticas precisam especificar dois chamáveis. Para lw (palavra de carregamento), isso seria: escrito:

    semfunc: "&RV32ILw", "&RV32ILwChild"

A especificação de desmontagem é mais convencional. Nenhuma menção é feita instrução infantil. Para lw, seria:

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

As instruções de carregamento que precisam ser implementadas são:

  • lb: byte de carregamento.
  • lbu: byte de carregamento sem assinatura.
  • lh: carregar meia palavra.
  • lhu: carrega meia palavra sem assinatura.
  • lw: carregar palavra.

Vá em frente, faça as alterações e crie. Verifique a saída gerada. Assim como antes, você pode comparar seu trabalho riscv32i.isa e rv32i_instructions.h.

Obrigado por chegar até aqui. Esperamos que isso tenha sido útil.