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 namespacei_type
. Cada função de extrator é declarada comoinline
, recebe o tipouint_t
mais estreito que contém a largura do formato e retorna o tipoint_t
mais estreito (para assinado) ouuint_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 tipouint_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.
Adicionar instruções de ramificação e de salto e vinculação
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.