Tutorial do decodificador de instruções binárias

Os objetivos deste tutorial são:

  • Aprenda sobre a estrutura e a sintaxe do arquivo de descrição do formato binário.
  • Saiba como a descrição do formato binário corresponde à descrição da ISA.
  • Escreva as descrições binárias para o subconjunto de instruções RiscV RV32I.

Visão geral

Codificação de instruções binárias RiscV

A codificação de instruções binária é a maneira padrão de codificar instruções para execução em um microprocessador. Normalmente, elas são armazenadas em um arquivo executável, geralmente no formato ELF. As instruções podem ter largura fixa ou variável.

Normalmente, as instruções usam um pequeno conjunto de formatos de codificação, com cada formato personalizado para o tipo de instruções codificadas. Por exemplo, instruções de registro-registro podem usar um formato que maximiza o número de opcodes disponíveis, enquanto instruções de registro-imediato usam outro que troca o número de opcodes disponíveis para aumentar o tamanho do imediato que pode ser codificado. As instruções de ramificação e salto quase sempre usam formatos que maximizam o tamanho do imediato para oferecer suporte a ramificações com deslocamentos maiores.

Os formatos de instrução usados pelas instruções que queremos decodificar no simulador RiscV são os seguintes:

Formato R-Type, usado para instruções de registro-registro:

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

Formato I-Type, usado para instruções imediatas de registro, instruções de carregamento e instrução jalr, de 12 bits imediato.

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

Formato especializado do tipo I, usado para deslocamento com instruções imediatas, 5 bits imediatos:

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

Formato U-Type, usado para instruções imediatas longas (lui, auipc), 20 bits imediatos:

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

Formato B-Type, usado para ramificações condicionais, imediato de 12 bits.

31 30,25 24..20 19..15 14..12 11..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

Formato J-Type, usado para a instrução jal, 20 bits imediato.

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

Formato S-Type, usado para instruções de loja, imediato de 12 bits.

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

Como você pode ver nesses formatos, todas essas instruções têm 32 bits e os 7 bits mais baixos em cada formato são o campo de opcode. Além disso, embora vários formatos tenham o mesmo tamanho imediato, os bits deles são retirados de diferentes partes da instrução. Como veremos, a especificação do decodificador binário é capaz de expressar isso.

Descrição da codificação binária

A codificação binária da instrução é expressa no arquivo de descrição (.bin_fmt) do formato binário. Ele descreve a codificação binária das instruções em uma ISA para que um decodificador de instruções de formato binário possa ser gerado. O decodificador gerado determina o código de operação, extrai o valor do operando e dos campos imediatos para fornecer as informações necessárias pelo decodificador independente de codificação ISA descrito no tutorial anterior.

Neste tutorial, criaremos um arquivo de descrição de codificação binária para um subconjunto das instruções do RiscV32I necessárias para simular as instruções usadas em um pequeno programa "Hello World". Para mais detalhes sobre a ISA RiscV, consulte Especificações do Risc-V{.external}.

Comece abrindo o arquivo: riscv_bin_decoder/riscv32i.bin_fmt.

O conteúdo do arquivo é dividido em várias seções.

Primeiro, a definição de decoder.

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

Nossa definição de decodificador especifica o nome do decodificador RiscV32I, além de quatro outras informações. O primeiro é namespace, que define o namespace em que o código gerado será colocado. Em segundo lugar, o opcode_enum, que nomeia como o tipo de enumeração de opcode gerado pelo decodificador ISA deve ser referenciado no código gerado. Terceiro, includes {} especifica arquivos de inclusão necessários para o código gerado para esse decodificador. No nosso caso, esse é o arquivo produzido pelo decodificador ISA do tutorial anterior. Arquivos de inclusão adicionais podem ser especificados em uma definição de includes {} com escopo global. Isso é útil se vários decodificadores forem definidos e todos precisarem incluir alguns dos mesmos arquivos. A quarta é uma lista de nomes de grupos de instruções que compõem as instruções para as quais o decodificador é gerado. No nosso caso, há apenas um: RiscVInst32.

A seguir, há três definições de formato. Eles representam diferentes formatos de instrução para uma palavra de instrução de 32 bits usada pelas instruções já definidas no arquivo.

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

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

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

O primeiro define um formato de instrução de 32 bits chamado Inst32Format que tem dois campos: bits (25 bits de largura) e opcode (7 bits de largura). Cada campo é unsigned, o que significa que o valor será estendido por zero quando for extraído e colocado em um tipo inteiro C++. A soma das larguras dos bitfields precisa ser igual à largura do formato. A ferramenta vai gerar um erro se houver uma incompatibilidade. Esse formato não é derivado de nenhum outro, portanto, é considerado um formato de nível superior.

O segundo define um formato de instrução de 32 bits chamado IType que deriva de Inst32Format, fazendo com que esses dois formatos sejam relacionados. O formato contém cinco campos: imm12, rs1, func3, rd e opcode. O campo imm12 é signed, o que significa que o valor será estendido por sinal quando for extraído e colocado em um tipo inteiro C++. Observe que IType.opcode tem o mesmo atributo assinado/não assinado e se refere aos mesmos bits de palavra de instrução que Inst32Format.opcode.

O terceiro formato é personalizado usado apenas pela instrução fence, que é uma instrução já especificada e não precisa ser considerada neste tutorial.

Importante: reutilize nomes de campos em diferentes formatos relacionados, desde que representam os mesmos bits e tenham o mesmo atributo assinado/não assinado.

Depois das definições de formato em riscv32i.bin_fmt, vem uma definição de grupo de instruções. Todas as instruções em um grupo de instruções precisam ter o mesmo comprimento de bit e usar um formato derivado (talvez indiretamente) do mesmo formato de instrução de nível superior. Quando uma ISA pode ter instruções com diferentes comprimentos, um grupo de instruções diferente é usado para cada comprimento. Além disso, se a decodificação da ISA de destino depender de um modo de execução, como instruções Arm ou Thumb, um grupo de instruções separado será necessário para cada modo. O parser bin_fmt gera um decodificador binário para cada grupo de instruções.

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

O grupo de instruções define um nome RiscV32I, uma largura [32], o nome do tipo de enumeração de opcode para usar "OpcodeEnum" e um formato de instrução base. O tipo de enumeração de opcode precisa ser o mesmo produzido pelo decodificador de instruções independente de formato abordado no tutorial sobre o decodificador ISA.

Cada descrição de codificação de instrução consiste em três partes:

  • O nome do opcode, que precisa ser o mesmo usado na descrição do decodificador de instrução para que os dois funcionem juntos.
  • O formato de instrução a ser usado para o opcode. Esse é o formato usado para atender às referências a campos de bits na parte final.
  • Uma lista separada por vírgulas de restrições de campo de bit, ==, !=, <, <=, > e >= que precisam ser verdadeiras para que o opcode corresponda à palavra de instrução.

O analisador .bin_fmt usa todas essas informações para criar um decodificador que:

  • Fornece funções de extração (assinada/não assinada) conforme apropriado para cada campo de bit em todos os formatos. As funções do extrator são colocadas em namespaces nomeados pela versão em snake case do nome do formato. Por exemplo, as funções do extrator para o formato IType são colocadas no namespace i_type. Cada função de extrator é declarada como inline, recebe o tipo uint_t mais estreito que contém a largura do formato e retorna o tipo int_t mais estreito (para assinado) ou uint_t (para não assinado), que contém a largura do campo extraído. Por exemplo:
inline uint8_t ExtractOpcode(uint32_t value) {
  return value & 0x7f;
}
  • Uma função de decodificação para cada grupo de instruções. Ele retorna um valor do tipo OpcodeEnum e usa o tipo uint_t mais estreito que contém a largura do formato do grupo de instruções.

Executar o build inicial

Altere o diretório para riscv_bin_decoder e crie o projeto usando o seguinte comando:

$ cd riscv_bin_decoder
$ bazel build :all

Agora mude o diretório de volta para a raiz do repositório e vamos conferir as fontes que foram geradas. Para isso, mude o diretório para bazel-out/k8-fastbuild/bin/riscv_bin_decoder, supondo que você esteja em um host x86. Para outros hosts, o k8-fastbuild será outra string.

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

O arquivo de cabeçalho gerado (.h)

Abra o arquivo riscv32i_bin_decoder.h. A primeira parte do arquivo contém proteções padrão padrão, arquivos de inclusão e declarações de namespace. Depois, há uma função auxiliar com modelo no namespace internal. Essa função é usada para extrair campos de bits de formatos muito longos para caber em um número inteiro de 64 bits em C++.

#ifndef RISCV32I_BIN_DECODER_H
#define RISCV32I_BIN_DECODER_H

#include <iostream>
#include <cstdint>

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


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

namespace mpact {
namespace sim {
namespace codelab {


namespace internal {

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

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

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

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

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

}  // namespace internal

Após a seção inicial, há um conjunto de três namespaces, um para cada declaração format no arquivo riscv32i.bin_fmt:


namespace fence {

...

}  // namespace fence

namespace i_type {

...

}  // namespace i_type

namespace inst32_format {

...

}  // namespace inst32_format

Em cada um desses namespaces, é definida a função de extração de bitfield inline para cada campo de bit nesse formato. Além disso, o formato base duplica as funções de extração dos formatos derivados em que 1) os nomes de campo ocorrem apenas em um único nome de campo ou 2) em que os nomes de campo se referem ao mesmo tipo de campo (assinhado/não assinado e posições de bit) em cada formato em que ocorrem. Isso permite que campos de bits que descrevem os mesmos bits sejam extraídos usando funções no namespace de formato de nível superior.

As funções no namespace i_type são mostradas abaixo:

namespace i_type {

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

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

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

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

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

}  // namespace i_type

Por fim, a declaração da função do decodificador para o grupo de instruções RiscVInst32 é declarada. Ele usa um valor não assinado de 32 bits como o valor da palavra de instrução e retorna o membro da classe de enumeração OpcodeEnum que corresponde ou OpcodeEnum::kNone se não houver correspondência.

OpcodeEnum DecodeRiscVInst32(uint32_t inst_word);

O arquivo de origem gerado (.cc)

Agora abra riscv32i_bin_decoder.cc. A primeira parte do arquivo contém as declarações de #include e de namespace, seguidas pelas declarações de função do decodificador:

#include "riscv32i_bin_decoder.h"

namespace mpact {
namespace sim {
namespace codelab {

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

O DecodeRiscVInst32None é usado para ações de decodificação vazias, ou seja, aquelas que retornam OpcodeEnum::kNone. As outras três funções compõem o decodificador gerado. O decodificador geral funciona de maneira hierárquica. Um conjunto de bits na palavra de instrução é calculado para diferenciar entre instruções ou grupos de instruções no nível superior. Os bits não precisam ser contíguos. O número de bits determina o tamanho de uma tabela de pesquisa que é preenchida com funções de decodificador de segundo nível. Isso é visto na próxima seção do arquivo:

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

    ...

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

    ...
};

Por fim, as funções do decodificador são definidas:

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

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

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

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

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

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

Nesse caso, em que há apenas quatro instruções definidas, há apenas um único nível de decodificação e uma tabela de consulta muito esparsa. À medida que as instruções são adicionadas, a estrutura do decodificador muda e o número de níveis na hierarquia da tabela do decodificador pode aumentar.


Adicionar instruções de ALU de registro-registro

Agora é hora de adicionar algumas novas instruções ao arquivo riscv32i.bin_fmt. O primeiro grupo de instruções são instruções de ALU de registro, como add, and etc. No RiscV32, todas elas usam o formato de instrução binária do tipo R:

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

A primeira coisa que precisamos fazer é adicionar o formato. Abra riscv32i.bin_fmt no seu editor favorito. Logo após o Inst32Format, é possível adicionar um formato chamado RType, que é derivado de Inst32Format. Todos os bitfields em RType são unsigned. Use os nomes, a largura dos bits e a ordem (da esquerda para a direita) da tabela acima para definir o formato. Se você precisar de uma dica ou quiser ver a solução completa, clique aqui.

Em seguida, precisamos adicionar as instruções. As instruções são:

  • add: adição de número inteiro.
  • and: bit a bit e.
  • or: OR bit a bit.
  • sll: desloca para a esquerda lógico.
  • sltu: define menos que, sem sinal.
  • sub: subtração de números inteiros.
  • xor: XOR bit a bit.

As codificações deles são:

31 a 25 24..20 19..15 14..12 11..7 6..0 nome do código de operação
000 0000 rs2 rs1 000 º 011 0011 adicionar
000 0000 rs2 rs1 111 a 011 0011 e
000 0000 rs2 rs1 110 º 011 0011 ou
000 0000 rs2 rs1 001 a 011 0011 sll
000 0000 rs2 rs1 011 º 011 0011 Slatu
010 0000 rs2 rs1 000 º 011 0011 sub
000 0000 rs2 rs1 100 º 011 0011 xor
func7 func3 código de operação

Adicione essas definições de instrução antes das outras instruções no grupo de instruções RiscVInst32. As strings binárias são especificadas com um prefixo inicial de 0b (semelhante a 0x para números hexadecimais). Para facilitar a leitura de strings longas de dígitos binários, você também pode inserir as aspas simples ' como um separador de dígitos no lugar certo.

Cada uma dessas definições de instrução terá três restrições, ou seja, em func7, func3 e opcode. Para todos, exceto sub, a restrição func7 será a seguinte:

func7 == 0b000'0000

A restrição func3 varia na maioria das instruções. Para add e sub, é:

func3 == 0b000

A restrição opcode é a mesma para cada uma dessas instruções:

opcode == 0b011'0011

Lembre-se de encerrar cada linha com um ponto e vírgula (;).

Confira a solução completa aqui.

Agora crie seu projeto como antes e abra o arquivo riscv32i_bin_decoder.cc gerado. Você vai notar que outras funções de decodificador foram geradas para processar as novas instruções. Na maioria das vezes, eles são semelhantes aos gerados antes, mas observe DecodeRiscVInst32_0_c, que é usado para a decodificação de add/sub:

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

Nessa função, há uma tabela de decodificação estática gerada, e um valor de pesquisa é extraído da palavra de instrução para selecionar o índice apropriado. Isso adiciona uma segunda camada na hierarquia do decodificador de instruções, mas, como o opcode pode ser procurado diretamente em uma tabela sem outras comparações, ele é inline nessa função em vez de exigir outra chamada de função.


Adicionar instruções de ALU com imediatos

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

O formato imediato do tipo I:

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

O formato imediato especializado I-Type:

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

O formato imediato do tipo U:

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

O formato I-Type já existe em riscv32i.bin_fmt. Portanto, não é necessário adicionar esse formato.

Se compararmos o formato especializado do tipo I com o formato do tipo R definido no exercício anterior, a única diferença é que os campos rs2 são renomeados para uimm5. Em vez de adicionar um formato totalmente novo, podemos aumentar o formato R-Type. Não podemos adicionar outro campo, porque isso aumentaria a largura do formato, mas podemos adicionar uma sobreposição. Uma sobreposição é um alias para um conjunto de bits no formato e pode ser usada para combinar várias subseqências do formato em uma entidade nomeada separada. O efeito colateral é que o código gerado agora também inclui uma função de extração para a sobreposição, além das funções para os campos. Nesse caso, quando rs2 e uimm5 não têm assinatura, não faz muita diferença, exceto para deixar explícito que o campo é usado como imediato. Para adicionar uma sobreposição chamada uimm5 ao formato R-Type, adicione o seguinte após o último campo:

  overlays:
    unsigned uimm5[5] = rs2;

O único formato novo que precisamos adicionar é o formato U-Type. Antes de adicionar o formato, vamos considerar as duas instruções que usam esse formato: auipc e lui. Ambos deslocam o valor imediato de 20 bits para a esquerda em 12 antes de usá-lo para adicionar o pc a ele (auipc) ou gravar diretamente em um registro (lui). Usando uma sobreposição, podemos fornecer uma versão pré-deslocada do imediato, deslocando um pouco da computação da execução da instrução para a decodificação da instrução. Primeiro, adicione o formato de acordo com os campos especificados na tabela acima. Em seguida, podemos adicionar a seguinte sobreposição:

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

A sintaxe de sobreposição permite concatenar não apenas campos, mas também literais. Neste caso, a concatenamos com 12 zeros, deslocando-a para a esquerda em 12.

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

  • addi: adicione imediatamente.
  • andi: bit a bit e com imediato.
  • ori: OR bit a bit com imediato.
  • xori: XOR bit a bit com imediato.

As codificações deles são:

31..20 19 a 15 14..12 11..7 6..0 opcode_name
imm12 rs1 000 º 001 0011 addi
imm12 rs1 111 a 001 0011 andi
imm12 rs1 110 º 001 0011 ori
imm12 rs1 100 a 001 0011 xori
func3 código de operação

As instruções R-Type (I-Type especializada) que precisamos adicionar são:

  • slli: deslocamento lógico para a esquerda por imediato.
  • srai: desloca a aritmética para a direita de forma imediata.
  • srli: desloca a lógica à direita imediatamente.

As codificações são:

31 a 25 24..20 19..15 14..12 11..7 6..0 nome do código de operação
000 0000 uimm5 rs1 001 º 001 0011 slli
010 0000 uimm5 rs1 101 º 001 0011 srai
000 0000 uimm5 rs1 101 º 001 0011 Srli
func7 func3 código de operação

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

  • auipc: adiciona o valor superior imediato ao pc.
  • lui: carrega a parte de cima imediatamente.

As codificações são:

31 a 12 11..7 6..0 nome do código de operação
uimm20 º 001 0111 auipc
uimm20 º 011 0111 lui
código de operação

Faça as mudanças e crie. Confira a saída gerada. Assim como antes, você pode verificar seu trabalho em relação a riscv32i.bin_fmt.


O próximo conjunto de instruções que precisa ser definido são as instruções de ramificação condicional, a instrução de salto e link e a instrução de salto e link do registro.

As ramificações condicionais que estamos adicionando usam a codificação do tipo B.

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

Embora a codificação do tipo B seja idêntica no layout à codificação do tipo R, optamos por usar um novo tipo de formato para que ele se alinhe à documentação do RiscV. No entanto, você também pode adicionar uma sobreposição para receber o deslocamento de ramificação correto imediatamente, usando os campos func7 e rd da codificação R-Type.

Adicionar um formato BType com os campos especificados acima é necessário, mas não é suficiente. Como você pode ver, o imediato é dividido em dois campos de instrução. Além disso, as instruções de ramificação não tratam isso como uma concatenação simples dos dois campos. Em vez disso, cada campo é particionado, e essas partições são concatenadas em uma ordem diferente. Por fim, esse valor é deslocado para a esquerda em um para conseguir um deslocamento alinhado de 16 bits.

A sequência de bits na palavra de instrução usada para formar o imediato é: 31, 7, 30..25, 11..8. Isso corresponde às seguintes referências de subcampo, em que o índice ou o intervalo especificam os bits no campo, numerados da direita para a esquerda, ou seja, imm7[6] se refere ao MSB de imm7, e imm5[0] se refere ao MSB de imm5.

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

Fazer com que essa manipulação de bits faça parte das instruções de ramificação tem duas grandes desvantagens. Primeiro, ele vincula a implementação da função semântica a detalhes na representação de instrução binária. Em segundo lugar, ele adiciona mais sobrecarga de execução. A resposta é adicionar uma sobreposição ao formato BType, incluindo um '0' final para representar o deslocamento para a esquerda.

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

A sobreposição é assinada, então ela será estendida automaticamente quando for extraída da palavra de instrução.

A instrução de salto e vinculação (imediata) usa a codificação do tipo J:

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

Esse também é um formato fácil de adicionar, mas, novamente, o imediato usado pela instrução não é tão simples quanto parece. As sequências de bits usadas para formar o imediato completo são: 31, 19..12, 20, 30..21, e o imediato final é deslocado para a esquerda em um para o alinhamento de meia palavra. A solução é adicionar outra sobreposição (21 bits para considerar o deslocamento para a esquerda) ao formato:

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

Como você pode ver, a sintaxe de sobreposições oferece suporte à especificação de vários intervalos em um campo em um formato abreviado. Além disso, se nenhum nome de campo for usado, os números de bit serão referentes à própria palavra de instrução. Portanto, o exemplo acima pode ser escrito como:

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

Por fim, o salto e vinculação (registro) usa o formato do tipo I usado anteriormente.

O formato imediato do tipo I:

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

Desta vez, não é necessário fazer mudanças no formato.

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

  • beq: ramificação se for igual.
  • bge: ramificação se maior ou igual.
  • bgeu: bifurcação se for maior ou igual a unsigned.
  • blt: ramificação se for menor que.
  • bltu: ramificação se for menor que não assinado.
  • bne: ramificação, se não for igual.

Eles são codificados da seguinte forma:

31..25 24..20 19..15 14..12 11..7 6..0 nome do código de operação
imm7 rs2 rs1 000 imm5 110 0011 beq
imm7 rs2 rs1 101 imm5 110 0011 bge
imm7 rs2 rs1 111 imm5 110 0011 Bgeu
imm7 rs2 rs1 100 imm5 110 0011 blt
imm7 rs2 rs1 110 imm5 110 0011 bltu
imm7 rs2 rs1 001 imm5 110 0011 bne
func3 código de operação

A instrução jal é codificada da seguinte maneira:

31..12 11..7 6..0 nome do código de operação
imm20 a 110 1111 Jal
código de operação

A instrução jalr é codificada da seguinte maneira:

31..20 19 a 15 14..12 11..7 6..0 opcode_name
imm12 rs1 000 º 110 0111 jalr
func3 código de operação

Faça as mudanças e crie. Verifique a saída gerada. Assim como antes, você pode verificar seu trabalho em relação a riscv32i.bin_fmt.


Adicionar instruções da loja

As instruções de armazenamento usam a codificação do tipo S, que é idêntica à codificação do tipo B usada por instruções de ramificação, exceto a composição do imediato. Optamos por adicionar o formato SType para permanecer alinhado com a documentação do RiscV.

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

No caso do formato SType, o imediato é felizmente uma concatenação simples dos dois campos imediatos. Portanto, a especificação de sobreposição é simplesmente:

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

Nenhum especificador de intervalo de bits é necessário ao concatenar campos inteiros.

As instruções de armazenamento são codificadas da seguinte maneira:

31 a 25 24..20 19..15 14..12 11..7 6..0 nome do código de operação
imm7 rs2 rs1 000 imm5 010 0011 sb
imm7 rs2 rs1 001 imm5 010 0011 sh
imm7 rs2 rs1 010 imm5 010 0011 sw
func3 código de operação

Faça as mudanças e crie. Confira a saída gerada. Assim como antes, você pode verificar seu trabalho em relação a riscv32i.bin_fmt.


Adicionar instruções de carregamento

As instruções de carregamento usam o formato I-Type. Nenhuma alteração precisa ser feita lá.

As codificações são:

31 a 20 19 a 15 14..12 11..7 6..0 opcode_name
imm12 rs1 000 º 000 0011 lb
imm12 rs1 100 º 000 0011 lbu
imm12 rs1 001 a 000 0011 lh
imm12 rs1 101 º 000 0011 lhu
imm12 rs1 010 º 000 0011 lw
func3 código de operação

Faça as mudanças e crie. Confira a saída gerada. Assim como antes, você pode verificar seu trabalho em relação a riscv32i.bin_fmt.

Este tutorial foi útil.