Os objetivos deste tutorial são:
- Conheça 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 do RiscV RV32I.
Visão geral
Codificação da instrução binária RiscV
A codificação de instruções binárias é a forma padrão de codificar instruções para a 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 largura.
Normalmente, as instruções usam um pequeno conjunto de formatos de codificação, com cada formato personalizados para o tipo de instruções codificadas. Por exemplo, registrar-registro podem usar um formato que maximize o número de códigos de operação disponíveis, enquanto as instruções imediatas de registro usam outra que troca o número de códigos de operação disponíveis para aumentar o tamanho do imediato que pode ser codificado. As instruções Branch e jump quase sempre usam formatos que maximizam o tamanho de o imediato para dar suporte a ramificações com deslocamentos maiores.
Os formatos de instrução usados pelas instruções que queremos decodificar no RiscV simulador são os seguintes:
Formato R-Type, usado para instruções de registro:
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 |
formato I-Type, usado para instruções imediatas de registro, instruções de carregamento e
a instrução jalr
, 12 bits imediato.
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 |
Formato I-Type especializado, usado para deslocamento com instruções imediatas, 5 bits imediato:
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 |
Formato U-Type, usado para instruções imediatas longas (lui
, auipc
), 20 bits
imediato:
31 a 12 | 11 a 7 | 6 a 0 |
---|---|---|
20 | 5 | 7 |
uimm20 | a | código de operação |
Formato de tipo B, usado para ramificações condicionais, imediato de 12 bits.
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 |
Formato J-Type, usado para a instrução jal
, 20 bits imediato.
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 |
Formato S-Type, usado para instruções de loja, imediato de 12 bits.
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 é possível ver nesses formatos, todas essas instruções têm 32 bits, e os 7 bits baixos em cada formato é o campo de código de operação. Além disso, embora vários formatos têm imediatamente o mesmo tamanho, os bits são retirados diferentes partes da instrução. Como veremos, o decodificador binário formato de especificação é capaz de expressar isso.
Descrição da codificação binária
A codificação binária da instrução é expressa no formato binário
(.bin_fmt
). Ela descreve a codificação binária do
em uma ISA para que um decodificador de instrução de formato binário
gerada. O decodificador gerado determina o código de operação, extrai o valor do
operando e campos imediatos, a fim de fornecer informações necessárias para o ISA
decodificador independente de codificação 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 "Hello World" neste programa. Para mais detalhes sobre o ISA do RiscV, acesse 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;
};
A definição do decodificador especifica o nome do nosso decodificador RiscV32I
, assim como
quatro informações adicionais. O primeiro é namespace
, que define
o namespace em que o código gerado será colocado. Em segundo lugar,
opcode_enum
, que nomeia como o tipo de enumeração do código de operação é gerado
pelo decodificador ISA deve ser referenciado no código gerado. Em terceiro lugar,
includes {}
especifica os arquivos incluídos necessários para o código gerado para:
desse decodificador.
No nosso caso, esse é o arquivo produzido pelo decodificador ISA na
tutorial anterior.
Arquivos de inclusão adicionais podem ser especificados em um includes {}
com escopo global
definição. Isso é útil quando vários decodificadores são definidos e todos precisam
incluir alguns dos mesmos arquivos. O quarto é uma lista de nomes de instruções
grupos que compõem as instruções para as quais o decodificador é gerado. Em nossa
caso haja apenas um: RiscVInst32
.
A seguir, há três definições de formato. Eles representam diferentes instruções formatos 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];
};
A primeira define um formato de instrução largo 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 colocada em um tipo C++ inteiro. A soma das larguras dos bitfields precisa
igual à largura do formato. A ferramenta vai gerar um erro se houver
discordância. Este formato não deriva de nenhum outro formato, portanto é
considerado um formato de nível superior.
A segunda define um formato de instrução largo de 32 bits chamado IType
, que deriva
de Inst32Format
, o que torna esses dois formatos relacionados. O formato contém 5
Campos: imm12
, rs1
, func3
, rd
e opcode
. O campo imm12
é
signed
, 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 refere-se aos mesmos bits de palavra de instrução
como Inst32Format.opcode
.
O terceiro é um formato personalizado usado apenas pelo fence
.
que é uma instrução já especificada e não temos
com as quais se preocupar neste tutorial.
Importante: reutilize nomes de campo em diferentes formatos relacionados, desde que eles representam os mesmos bits e têm o mesmo atributo assinado/não assinado.
Depois das definições de formato no riscv32i.bin_fmt
, é gerado um grupo de instruções.
definição. Todas as instruções em um grupo de instruções devem ter o mesmo
comprimento de bits e usar um formato que deriva (talvez indiretamente) da mesma
formato de instrução de alto nível. Quando uma ISA pode ter instruções com diferentes
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 Arm vs. Thumb
instruções, um grupo de instruções separado é necessário para cada modo. A
O analisador 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 de
o tipo de enumeração do código de operação para usar "OpcodeEnum"
e uma instrução base;
. O tipo de enumeração do código de operação deve ser o mesmo produzido pelo
decodificador de instruções independente de formato abordado no tutorial da ISA
decodificador.
Cada descrição de codificação de instrução consiste em 3 partes:
- O nome do código de operação, que precisa ser o mesmo usado na instrução do decodificador para que os dois funcionem juntos.
- O formato de instrução a ser usado para o código de operação. Esse é o formato usada para satisfazer as referências a bitfields na parte final.
- Uma lista separada por vírgulas de restrições de campo de bits,
==
,!=
,<
,<=
,>
, e>=
que todos precisam ser verdadeiros para que o código de operação corresponda ao ou palavra instrutiva.
O analisador .bin_fmt
usa todas essas informações para criar um decodificador que:
- Fornece funções de extração (assinadas/não assinadas) conforme apropriado para cada bit
em todos os formatos. As funções do extrator são colocadas em namespaces
nomeada pela versão snake-case do nome do formato. Por exemplo, o
as funções de extração para o formato
IType
são colocadas no namespacei_type
. Cada função do extrator é declaradainline
, usa auint_t
mais estreita que contém a largura do formato e retorna oint_t
mais estreito (para assinado), tipouint_t
(para não assinado), que contém o campo extraído largura. Por exemplo:
inline uint8_t ExtractOpcode(uint32_t value) {
return value & 0x7f;
}
- Uma função de decodificação para cada grupo de instruções. Retorna um valor do tipo
OpcodeEnum
e usa o tipo deuint_t
mais estreito que contém a largura do o formato do grupo de instruções.
Executar o build inicial
Mude o diretório para riscv_bin_decoder
e crie o projeto usando o
seguinte comando:
$ cd riscv_bin_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_bin_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_bin_decoder
riscv32i_bin_decoder.h
riscv32i_bin_decoder.cc
O arquivo principal (.h) gerado
Abra o arquivo riscv32i_bin_decoder.h
. A primeira parte do arquivo contém padrões
de proteção boilerplate, arquivos de inclusão e declarações de namespace. Depois disso
há uma função auxiliar com modelo no namespace internal
. Essa função
é usado para extrair campos de bit de formatos muito longos para caber em um C++ de 64 bits
inteiro.
#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
Na seção inicial, há um conjunto de três namespaces, um para cada
das declarações 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, 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 descendentes para os quais
1) os nomes dos campos só ocorrerem em um único nome de campo ou 2) para os quais o
nomes de campo referem-se ao mesmo tipo de campo (posições assinada/não assinada e bit)
em cada formato em que ocorrem. Isso ativa campos de bit que descrevem o mesmo
bits a serem 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 decodificador para a instrução
o grupo RiscVInst32
foi declarado. Ele usa um valor não assinado de 32 bits como valor de
a 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 (.cc) gerado
Agora abra riscv32i_bin_decoder.cc
. A primeira parte do arquivo contém
as declarações #include
e de namespace, seguidas pela função decodificador
declarações:
#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,
que retornam OpcodeEnum::kNone
. As outras três funções compõem a
decodificador gerado. O decodificador geral funciona de maneira hierárquica. Um conjunto
de bits na palavra de instrução é calculado para diferenciar
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 consulta que
é preenchido com funções decodificadores de segundo nível. Isso será visto nos próximos
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 decodificadores 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 nível único de decodificação e uma tabela de consulta muito esparsa. Como as instruções são a estrutura do decodificador será alterada e o número de níveis na a hierarquia da tabela de decodificador pode aumentar.
Adicionar instruções de ALU de registro/registro
Agora é hora de adicionar algumas novas instruções ao arquivo riscv32i.bin_fmt
. A
o primeiro grupo de instruções são instruções ALU de registro-registro, como
add
, and
etc. No RiscV32, todos eles usam a instrução binária do tipo R.
formato:
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 |
A primeira coisa que precisamos fazer é adicionar o formato. Abra
riscv32i.bin_fmt
no seu editor favorito. Logo após a Inst32Format
,
adicione um formato chamado RType
, que deriva 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
: 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.
As codificações deles são:
31 a 25 | 24 a 20 | 19 a 15 | 14 a 12 | 11 a 7 | 6 a 0 | nome do código de operação |
---|---|---|---|---|---|---|
000 0000 | rs2 | rs1 | 000 | a | 011 0011 | adicionar |
000 0000 | rs2 | rs1 | 111 | a | 011 0011 | e |
000 0000 | rs2 | rs1 | 110 | a | 011 0011 | ou |
000 0000 | rs2 | rs1 | 001 | a | 011 0011 | Sll |
000 0000 | rs2 | rs1 | 011 | a | 011 0011 | Slatu |
010 mil | rs2 | rs1 | 000 | a | 011 0011 | sub |
000 0000 | rs2 | rs1 | 100 | a | 011 0011 | xor |
func7 | func3 | código de operação |
Adicione essas definições de instrução antes das outras instruções na
RiscVInst32
grupo de instruções. Strings binárias são especificadas com um
Prefixo de 0b
(semelhante a 0x
para números hexadecimais). Para facilitar
ler strings longas de dígitos binários, você também pode inserir as aspas simples '
como
um separador de dígitos
onde você achar necessário.
Cada uma dessas definições de instrução tem três restrições:
func7
, func3
e opcode
. Para todos, exceto sub
, a restrição func7
ser:
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 todas estas instruções:
opcode == 0b011'0011
Lembre-se de encerrar cada linha com um ponto e vírgula (;
).
A solução final é aqui.
Agora, crie seu projeto como antes, depois abra a
riscv32i_bin_decoder.cc
. Você vai notar que outras funções de decodificador
foram geradas para lidar com as novas instruções. A maioria deles 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, é gerada uma tabela de decodificação estática, e um valor de pesquisa é extraído da palavra de instrução para selecionar o índice adequado. Isso adiciona um segunda camada na hierarquia do decodificador de instruções, mas, como o código de operação pode ser pesquisado diretamente em uma tabela sem comparações adicionais, está embutido neste em vez de exigir outra chamada de função.
Adicionar instruções de ALU com imediatamente
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 |
O formato I-Type já existe em riscv32i.bin_fmt
, então não é necessário
adicionar esse formato.
Se compararmos o formato I-Type especializado com o formato R-Type que definimos no
no exercício anterior, vemos que a única diferença é que os campos rs2
é renomeada como uimm5
. Em vez de adicionar um formato totalmente novo, podemos aumentar
Formato R-Type. Não podemos adicionar outro campo, pois isso aumentaria a largura do
o formato, mas podemos adicionar uma sobreposição. Uma sobreposição é um alias para um conjunto de
bits no formato e podem ser usadas para combinar várias subsequências da
em uma entidade nomeada separada. O efeito colateral é que o código gerado
agora também incluirá uma função de extração para a sobreposição, além da
os campos. Nesse caso, quando rs2
e uimm5
não estão assinados,
não faz muita diferença, exceto deixar claro que o campo é usado
imediatamente. Para adicionar uma sobreposição chamada uimm5
ao formato R-Type, adicione o
após o último campo:
overlays:
unsigned uimm5[5] = rs2;
O único formato novo que precisamos adicionar é o formato U-Type. Antes de adicionarmos o
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
adicionar o pc a ele (auipc
) ou gravá-lo diretamente em um registrador
lui
). Usando uma sobreposição, podemos fornecer uma versão pré-deslocada do imediato,
mudar um pouco do cálculo da execução da instrução para a instrução
decodificação. Primeiro adicione o formato de acordo com os campos especificados na tabela.
acima. Então, podemos adicionar a seguinte sobreposição:
overlays:
unsigned uimm32[32] = uimm20, 0b0000'0000'0000;
A sintaxe de sobreposição nos permite concatenar, não apenas campos, mas literais como muito bem. Neste caso, a concatenamos com 12 zeros, deslocando-a para a esquerda até 12.
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 codificações deles são:
31 a 20 | 19 a 15 | 14 a 12 | 11 a 7 | 6 a 0 | opcode_name |
---|---|---|---|---|---|
imm12 | rs1 | 000 | a | 001 0011 | Addi |
imm12 | rs1 | 111 | a | 001 0011 | andi |
imm12 | rs1 | 110 | a | 001 0011 | Ori |
imm12 | rs1 | 100 | a | 001 0011 | xori |
func3 | código de operação |
As instruções do R-Type (I-Type especializado) 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 codificações deles são:
31 a 25 | 24 a 20 | 19 a 15 | 14 a 12 | 11 a 7 | 6 a 0 | nome do código de operação |
---|---|---|---|---|---|---|
000 0000 | uimm5 | rs1 | 001 | a | 001 0011 | Slli |
010 mil | uimm5 | rs1 | 101 | a | 001 0011 | srai |
000 0000 | uimm5 | rs1 | 101 | a | 001 0011 | Srli |
func7 | func3 | código de operação |
As instruções do tipo u que precisamos adicionar são:
auipc
: adicionar o imediatamente superior ao PC.lui
: carregar imediatamente superior.
As codificações deles são:
31 a 12 | 11 a 7 | 6 a 0 | nome do código de operação |
---|---|---|---|
uimm20 | a | 001 0111 | auipc |
uimm20 | a | 011 0111 | lui |
código de operação |
Vá em frente, faça as alterações e crie. Verifique a saída gerada. Assim como antes, você pode comparar seu trabalho riscv32i.bin_fmt (link em inglês).
Adicionar instruções de ramificação e link direto
O próximo conjunto de instruções que precisa ser definido é a ramificação condicional de links, a instrução de jump-and-link e o registro de jump-and-link instrução.
As ramificações condicionais que estamos adicionando usam a codificação B-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 |
imm7 | rs2 | rs1 | func3 | imm5 | código de operação |
Embora o layout da codificação B-Type seja idêntico à codificação R-Type, nós
escolha usar um novo tipo de formato para alinhá-lo com a documentação do RiscV.
Mas você também poderia ter adicionado apenas uma sobreposição para obter a ramificação apropriada
deslocamento imediato, usando os campos func7
e rd
do tipo R-Type
e codificação.
Adicionar um formato BType
com os campos especificados acima é necessário, mas não
suficientes. Como você pode ver, o imediato é dividido em dois campos de instrução.
Além disso, as instruções da ramificação não tratam isso como uma simples concatenação de
os dois campos. Em vez disso, cada campo é particionado ainda mais, e essas partições
são concatenados em uma ordem diferente. Por fim, esse valor é deslocado para a esquerda
um para obter 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 intervalo especifica os bits no campo, numerados da direita para a esquerda, isto é,
imm7[6]
refere-se ao msb de imm7
, e imm5[0]
refere-se ao lsb de
imm5
.
imm7[6], imm5[0], imm7[5..0], imm5[4..1]
Tornar essa manipulação parte das instruções da ramificação tem duas
grandes desvantagens. Primeiro, vincula a implementação da função semântica ao
na representação da instrução binária. Segundo, aumenta o tempo de execução
sobrecarga. A resposta é adicionar uma sobreposição ao formato BType
, incluindo
"0" à direita para considerar o deslocamento à esquerda.
overlays:
signed b_imm[13] = imm7[6], imm5[0], imm7[5..0], imm5[4..1], 0b0;
Observe que a sobreposição é assinada, então será automaticamente estendida quando extraído da palavra de instrução.
A instrução jump-and-link (imediata) usa a codificação J-Type:
31 a 12 | 11 a 7 | 6 a 0 |
---|---|---|
20 | 5 | 7 |
imm20 | a | código de operação |
Esse também é um formato fácil de adicionar, mas, novamente, o imediato que é usado pelo de ensino não é tão simples quanto parece. As sequências de bits usadas para do imediato total 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 à esquerda) para o formato:
overlays:
signed j_imm[21] = imm20[19, 7..0, 8, 18..9], 0b0;
Como você pode ver, a sintaxe das sobreposições permite especificar vários intervalos em um em um formato abreviado. Além disso, se nenhum nome de campo for usado, o bit os números se referem à própria palavra de instrução, então o código acima poderia muito bem ser escrito como:
signed j_imm[21] = [31, 19..12, 20, 30..21], 0b0;
Finalmente, o jump-and-link (registro) usa o formato I-type conforme utilizado antes.
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 |
Desta vez, não é preciso fazer nenhuma alteração no formato.
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.
Eles são codificados da seguinte forma:
31 a 25 | 24 a 20 | 19 a 15 | 14 a 12 | 11 a 7 | 6 a 0 | nome do código de operação |
---|---|---|---|---|---|---|
imm7 | rs2 | rs1 | 000 | imm5 | 110.011 | beq |
imm7 | rs2 | rs1 | 101 | imm5 | 110.011 | bge |
imm7 | rs2 | rs1 | 111 | imm5 | 110.011 | Bgeu |
imm7 | rs2 | rs1 | 100 | imm5 | 110.011 | blt |
imm7 | rs2 | rs1 | 110 | imm5 | 110.011 | bltu |
imm7 | rs2 | rs1 | 001 | imm5 | 110.011 | bne |
func3 | código de operação |
A instrução jal
é codificada da seguinte maneira:
31 a 12 | 11 a 7 | 6 a 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 a 20 | 19 a 15 | 14 a 12 | 11 a 7 | 6 a 0 | opcode_name |
---|---|---|---|---|---|
imm12 | rs1 | 000 | a | 110.0111 | Jalr |
func3 | código de operação |
Vá em frente, faça as alterações e crie. Verifique a saída gerada. Assim como antes, você pode comparar seu trabalho riscv32i.bin_fmt (link em inglês).
Adicionar instruções da loja
As instruções da loja usam a codificação S-Type, que é idêntica à B-Type.
codificação usada por instruções de ramificação, exceto para a composição do
imediato. Escolhemos adicionar o formato SType
para permanecer alinhado ao RiscV
na documentação do Google Cloud.
31 a 25 | 24 a 20 | 19 a 15 | 14 a 12 | 11 a 7 | 6 a 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, um direto
concatenação direta dos dois campos imediatos, de modo que a especificação da sobreposição
é simplesmente:
overlays:
signed s_imm[12] = imm7, imm5;
Observe que nenhum especificador de intervalo de bits é necessário ao concatenar campos inteiros.
As instruções de armazenamento são codificadas da seguinte forma:
31 a 25 | 24 a 20 | 19 a 15 | 14 a 12 | 11 a 7 | 6 a 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 |
Vá em frente, faça as alterações e crie. Verifique a saída gerada. Assim como antes, você pode comparar seu trabalho riscv32i.bin_fmt (link em inglês).
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 a 12 | 11 a 7 | 6 a 0 | opcode_name |
---|---|---|---|---|---|
imm12 | rs1 | 000 | a | 000 0011 | lb |
imm12 | rs1 | 100 | a | 000 0011 | lbu |
imm12 | rs1 | 001 | a | 000 0011 | lh |
imm12 | rs1 | 101 | a | 000 0011 | lhu |
imm12 | rs1 | 010 | a | 000 0011 | lw |
func3 | código de operação |
Faça as alterações e crie. Verifique a saída gerada. Assim como antes, você pode comparar seu trabalho riscv32i.bin_fmt (link em inglês).
Esperamos que ele tenha sido útil para a conclusão deste tutorial.