Os objetivos deste tutorial são:
- Saiba como as instruções são representadas no simulador MPACT-Sim.
- Conheça a estrutura e a sintaxe do arquivo de descrição da ISA.
- Escreva as descrições da ISA para o subconjunto de instruções RiscV RV32I
Visão geral
No MPACT-Sim, as instruções de destino são decodificadas e armazenadas em uma representação interna para tornar as informações mais disponíveis e a semântica mais rápida para executar. Essas instâncias de instrução são armazenadas em cache em um cache de instruções para reduzir o número de vezes que as instruções executadas com frequência são executadas.
A classe de instrução
Antes de começar, é útil saber como as instruções são
representadas no MPACT-Sim. A classe Instruction
é definida em mpact-sim/mpact/sim/generic/instruction.h (em inglês).
A instância da classe Instruction contém todas as informações necessárias para simular a instrução quando ela é "executada", como:
- Endereço das instruções, tamanho da instrução simulada, por exemplo, tamanho em .texto.
- Opcode da instrução.
- Ponteiro da interface do operando predicado (se aplicável).
- Vetor de ponteiros da interface do operando de origem.
- Vetor de ponteiros da interface do operando de destino.
- Função semântica chamável.
- Ponteiro para o objeto de estado da arquitetura.
- Ponteiro para o objeto de contexto.
- Ponteiro para instâncias filhas e próximas de instrução.
- String de desmontagem.
Essas instâncias geralmente são armazenadas em um cache de instrução (instância) e são reutilizadas sempre que a instrução é executada novamente. Isso melhora o desempenho durante a execução.
Com exceção do ponteiro para o objeto de contexto, todos são preenchidos pelo decodificador de instruções gerado a partir da descrição da ISA. Neste tutorial, não é necessário conhecer os detalhes desses itens, porque eles não serão usados diretamente. Em vez disso, uma compreensão de alto nível sobre como eles são usados é suficiente.
A função semântica que pode ser chamada é o objeto de função/método/função C++
(incluindo lambdas) que implementa a semântica da instrução. Por
exemplo, para uma instrução add
, ele carrega cada operando de origem, adiciona os dois
operandos e grava o resultado em um único operando de destino. O tópico de
funções semânticas é abordado em detalhes no tutorial de funções semânticas.
Operadores de instrução
A classe de instrução inclui ponteiros para três tipos de interfaces de operando: predicado, origem e destino. Essas interfaces permitem que funções semânticas sejam escritas independentemente do tipo real do operando de instrução subjacente. Por exemplo, o acesso aos valores de registros e imediatos é feito pela mesma interface. Isso significa que as instruções que executam a mesma operação, mas em operandos diferentes (por exemplo, registros ou imedates), podem ser implementadas usando a mesma função semântica.
A interface de operando de predicado, para aquelas ISAs que oferecem suporte à execução de instruções preditas (para outras ISAs, ela é nula), é usada para determinar se uma determinada instrução precisa ser executada com base no valor booleano do predicado.
// The predicte operand interface is intended primarily as the interface to
// read the value of instruction predicates. It is separated from source
// predicates to avoid mixing it in with the source operands needed for modeling
// the instruction semantics.
class PredicateOperandInterface {
public:
virtual bool Value() = 0;
// Return a string representation of the operand suitable for display in
// disassembly.
virtual std::string AsString() const = 0;
virtual ~PredicateOperandInterface() = default;
};
A interface de operando de origem permite que a função semântica da instrução leia valores dos operandos de instruções sem considerar o tipo de operando subjacente. Os métodos de interface oferecem suporte a operandos com valores escalares e vetoriais.
// The source operand interface provides an interface to access input values
// to instructions in a way that is agnostic about the underlying implementation
// of those values (eg., register, fifo, immediate, predicate, etc).
class SourceOperandInterface {
public:
// Methods for accessing the nth value element.
virtual bool AsBool(int index) = 0;
virtual int8_t AsInt8(int index) = 0;
virtual uint8_t AsUint8(int index) = 0;
virtual int16_t AsInt16(int index) = 0;
virtual uint16_t AsUint16(int) = 0;
virtual int32_t AsInt32(int index) = 0;
virtual uint32_t AsUint32(int index) = 0;
virtual int64_t AsInt64(int index) = 0;
virtual uint64_t AsUint64(int index) = 0;
// Return a pointer to the object instance that implements the state in
// question (or nullptr) if no such object "makes sense". This is used if
// the object requires additional manipulation - such as a fifo that needs
// to be pop'ed. If no such manipulation is required, nullptr should be
// returned.
virtual std::any GetObject() const = 0;
// Return the shape of the operand (the number of elements in each dimension).
// For instance {1} indicates a scalar quantity, whereas {128} indicates an
// 128 element vector quantity.
virtual std::vector<int> shape() const = 0;
// Return a string representation of the operand suitable for display in
// disassembly.
virtual std::string AsString() const = 0;
virtual ~SourceOperandInterface() = default;
};
A interface de operandos de destino fornece métodos para alocar e processar
instâncias de DataBuffer
, que são o tipo de dados interno usado para armazenar valores de registro. Um
operando de destino também tem uma latência associada, que é o número
de ciclos a aguardar até que a instância do buffer de dados alocada pela função
semântica da instrução seja usada para atualizar o valor do registro de destino. Por
exemplo, a latência de uma instrução add
pode ser 1, enquanto para uma
instrução mpy
pode ser 4. Isso é abordado com mais detalhes no
tutorial sobre funções semânticas.
// The destination operand interface is used by instruction semantic functions
// to get a writable DataBuffer associated with a piece of simulated state to
// which the new value can be written, and then used to update the value of
// the piece of state with a given latency.
class DestinationOperandInterface {
public:
virtual ~DestinationOperandInterface() = default;
// Allocates a data buffer with ownership, latency and delay line set up.
virtual DataBuffer *AllocateDataBuffer() = 0;
// Takes an existing data buffer, and initializes it for the destination
// as if AllocateDataBuffer had been called.
virtual void InitializeDataBuffer(DataBuffer *db) = 0;
// Allocates and initializes data buffer as if AllocateDataBuffer had been
// called, but also copies in the value from the current value of the
// destination.
virtual DataBuffer *CopyDataBuffer() = 0;
// Returns the latency associated with the destination operand.
virtual int latency() const = 0;
// Return a pointer to the object instance that implmements the state in
// question (or nullptr if no such object "makes sense").
virtual std::any GetObject() const = 0;
// Returns the order of the destination operand (size in each dimension).
virtual std::vector<int> shape() const = 0;
// Return a string representation of the operand suitable for display in
// disassembly.
virtual std::string AsString() const = 0;
};
Descrição da ISA
A arquitetura do conjunto de instruções (ISA) de um processador define o modelo abstrato pelo qual o software interage com o hardware. Ele define o conjunto de instruções disponíveis, os tipos de dados, os registros e outros estados da máquina em que as instruções operam, bem como o comportamento delas (semântica). Para fins do MPACT-Sim, o ISA não inclui a codificação real das instruções. Isso é tratado separadamente.
A ISA do processador é expressa em um arquivo de descrição que descreve o conjunto de instruções em um nível abstrato e independente da codificação. O arquivo de descrição enumera o conjunto de instruções disponíveis. Para cada instrução, é obrigatório listar o nome, o número e os nomes dos operandos e a vinculação a uma função/chamada C++ que implementa a semântica. Além disso, é possível especificar uma string de formatação desassembly e o uso de nomes de recursos de hardware pela instrução. O primeiro é útil para produzir uma representação textual da instrução para depuração, rastreamento ou uso interativo. Esse último pode ser usado para aumentar a precisão do ciclo na simulação.
O arquivo de descrição da ISA é analisado pelo analisador de isa, que gera o código para o decodificador de instruções independente de representação. Esse decodificador é responsável por preencher os campos dos objetos de instrução. Os valores específicos, como o número de registro de destino, são recebidos de um decodificador de instruções de formato. Um desses decodificadores é o decodificador binário, que é o foco do próximo tutorial.
Neste tutorial, mostramos como escrever um arquivo de descrição de ISA para uma arquitetura simples e escalar. Usaremos um subconjunto do conjunto de instruções do RiscV RV32I para ilustrar isso e, com os outros tutoriais, criaremos um simulador capaz de simular um programa "Hello World". Para mais detalhes sobre o ISA do RiscV, consulte Especificações do RiscV.
Comece abrindo o arquivo:
riscv_isa_decoder/riscv32i.isa
O conteúdo do arquivo é dividido em várias seções. A primeira é a declaração da ISA:
isa RiscV32I {
namespace mpact::sim::codelab;
slots { riscv32; }
}
Isso declara RiscV32I
como o nome da ISA, e o gerador de código cria
uma classe chamada RiscV32IEncodingBase
que define a interface que o
decodificador gerado vai usar para receber informações sobre o código de operação e o operando. O nome
dessa classe é gerado convertendo o nome da ISA em Pascal-case e
concatenando-o com EncodingBase
. A declaração slots { riscv32; }
especifica que há apenas um único slot de instrução riscv32
no RiscV32I ISA (em vez de vários slots em uma instrução VLIW), e que as únicas instruções válidas são aquelas definidas para execução em riscv32
.
// First disasm fragment is 15 char wide and left justified.
disasm widths = {-15};
Isso especifica que o primeiro fragmento de desmontagem de qualquer especificação de dissasembly (veja mais abaixo) será justificado à esquerda em um campo de 15 caracteres. Todos os fragmentos subsequentes serão anexados a esse campo sem espaço adicional.
Abaixo, há três declarações de slot: riscv32i
, zicsr
e riscv32
.
Com base na definição de isa
acima, somente as instruções definidas para o slot riscv32
vão fazer parte da isa RiscV32I
. Para que servem os outros dois slots?
Os slots podem ser usados para fatorar instruções em grupos separados, que podem ser
combinados em um único slot no final. Observe a notação : riscv32i, zicsr
na declaração de slot riscv32
. Isso especifica que o slot riscv32
herda todas as instruções definidas nos slots zicsr
e riscv32i
. A ISA de 32 bits do RiscV
consiste em uma ISA de base chamada RV32I, à qual um conjunto de extensões opcionais pode
ser adicionado. O mecanismo de slot permite que as instruções nessas extensões sejam
especificadas separadamente e combinadas conforme necessário no final para definir a
ISA geral. Nesse caso, as instruções no grupo "I" do RiscV são definidas
separadamente das instruções no grupo "zicsr". Grupos adicionais podem ser definidos para "M" (multiplicar/dividir), "F" (ponto flutuante de precisão única), "D" (ponto flutuante de precisão dupla), "C" (instruções compactas de 16 bits) etc. conforme necessário para a ISA final do RiscV.
// The RiscV 'I' instructions.
slot riscv32i {
...
}
// RiscV32 CSR manipulation instructions.
slot zicsr {
...
}
// The final instruction set combines riscv32i and zicsr.
slot riscv32 : riscv32i, zicsr {
...
}
As definições dos slots zicsr
e riscv32
não precisam ser alteradas. No entanto, o foco deste tutorial é adicionar as definições necessárias ao slot riscv32i
. Vamos analisar melhor o que está definido atualmente nesse slot:
// The RiscV 'I' instructions.
slot riscv32i {
// Include file that contains the declarations of the semantic functions for
// the 'I' instructions.
includes {
#include "learning/brain/research/mpact/sim/codelab/riscv_semantic_functions/solution/rv32i_instructions.h"
}
// These are all 32 bit instructions, so set default size to 4.
default size = 4;
// Model these with 0 latency to avoid buffering the result. Since RiscV
// instructions have sequential semantics this is fine.
default latency = 0;
// The opcodes.
opcodes {
fence{: imm12 : },
semfunc: "&RV32IFence"c
disasm: "fence";
ebreak{},
semfunc: "&RV32IEbreak",
disasm: "ebreak";
}
}
Primeiro, há uma seção includes {}
que lista os arquivos principais que precisam
ser incluídos no código gerado quando esse slot é referenciado, direta ou
indiretamente, na ISA final. Os arquivos de inclusão também podem ser listados em uma seção includes {}
de escopo global,
caso em que eles são sempre incluídos. Isso pode
ser útil se o mesmo arquivo de inclusão precisasse ser adicionado a cada definição
de slot.
As declarações default size
e default latency
definem que, a menos que
especificado de outra forma, o tamanho de uma instrução é 4 e que a latência de
gravação de um operando de destino é de 0 ciclos. Observe que o tamanho da instrução
especificada aqui é o tamanho do incremento do contador do programa para calcular o
endereço da próxima instrução sequencial a ser executada no processador
simulado. Esse valor pode ou não ser igual ao tamanho em bytes da
representação de instrução no arquivo executável de entrada.
A seção de opcode é fundamental para a definição do slot. Como você pode ver, apenas dois
códigos de operação (instruções) fence
e ebreak
foram definidos até agora em
riscv32i
. O opcode fence
é definido especificando o nome (fence
) e
a especificação do operando ({: imm12 : }
), seguida pelo formato de desmontagem
opcional ("fence"
) e o elemento que será vinculado como a função
semântica ("&RV32IFence"
).
Os operandos de instrução são especificados como um triplo, com cada componente
separado por um ponto e vírgula, predicado ':' lista de operandos de origem ':'
lista de operandos de destino. As listas de operandos de origem e destino são listas de nomes de operandos separados por vírgulas. Como você pode notar, os operandos de instrução da
instrução fence
não contêm operandos de predicado, apenas um único nome de operando
de origem, imm12
e nenhum operando de destino. O subconjunto RiscV RV32I não
oferece suporte à execução predita. Portanto, o operando de predicado vai estar sempre vazio
neste tutorial.
A função semântica é especificada como a string necessária para especificar a função
ou o chamável C++ a ser usado para chamar a função semântica. A assinatura da
função semântica/chamável é void(Instruction *)
.
A especificação de desmontagem consiste em uma lista de strings separadas por vírgulas.
Normalmente, apenas duas strings são usadas, uma para o código de operação e outra para os
operantes. Quando formatadas (usando a chamada AsString()
na instrução), cada
string é formatada em um campo de acordo com a especificação disasm widths
descrita acima.
Os exercícios a seguir ajudam a adicionar instruções ao arquivo riscv32i.isa
suficientes para simular um programa "Hello World". Para quem está com pressa, as
soluções podem ser encontradas em
riscv32i.isa
e
rv32i_instructions.h.
Executar o build inicial
Se você não mudou o diretório para riscv_isa_decoder
, faça isso agora. Em seguida,
crie o projeto da seguinte maneira. Essa criação precisa ser bem-sucedida.
$ cd riscv_isa_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_isa_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_isa_decoder
Nesse diretório, entre outros arquivos, serão gerados os seguintes arquivos C++:
riscv32i_decoder.h
riscv32i_decoder.cc
riscv32i_enums.h
riscv32i_enums.cc
Para conferir o riscv32i_enums.h
, clique nele no navegador. Ele
deve conter algo como:
#ifndef RISCV32I_ENUMS_H
#define RISCV32I_ENUMS_H
namespace mpact {
namespace sim {
namespace codelab {
enum class SlotEnum {
kNone = 0,
kRiscv32,
};
enum class PredOpEnum {
kNone = 0,
kPastMaxValue = 1,
};
enum class SourceOpEnum {
kNone = 0,
kCsr = 1,
kImm12 = 2,
kRs1 = 3,
kPastMaxValue = 4,
};
enum class DestOpEnum {
kNone = 0,
kCsr = 1,
kRd = 2,
kPastMaxValue = 3,
};
enum class OpcodeEnum {
kNone = 0,
kCsrs = 1,
kCsrsNw = 2,
kCsrwNr = 3,
kEbreak = 4,
kFence = 5,
kPastMaxValue = 6
};
constexpr char kNoneName[] = "none";
constexpr char kCsrsName[] = "Csrs";
constexpr char kCsrsNwName[] = "CsrsNw";
constexpr char kCsrwNrName[] = "CsrwNr";
constexpr char kEbreakName[] = "Ebreak";
constexpr char kFenceName[] = "Fence";
extern const char *kOpcodeNames[static_cast<int>(
OpcodeEnum::kPastMaxValue)];
enum class SimpleResourceEnum {
kNone = 0,
kPastMaxValue = 1
};
enum class ComplexResourceEnum {
kNone = 0,
kPastMaxValue = 1
};
enum class AttributeEnum {
kPastMaxValue = 0
};
} // namespace codelab
} // namespace sim
} // namespace mpact
#endif // RISCV32I_ENUMS_H
Como você pode ver, cada slot, opcode e operando definido no
arquivo riscv32i.isa
é definido em um dos tipos de enumeração. Além disso, há uma matriz OpcodeNames
que armazena todos os nomes dos códigos de operação, que é definida em riscv32i_enums.cc
. Os outros arquivos contêm o decodificador gerado, que será abordado com mais detalhes em outro tutorial.
Regra de build do Bazel
O destino do decodificador de ISA no Bazel é definido usando uma macro de regra personalizada chamada
mpact_isa_decoder
, que é carregada de mpact/sim/decoder/mpact_sim_isa.bzl
no repositório mpact-sim
. Para este tutorial, o destino do build definido em
riscv_isa_decoder/BUILD
é:
mpact_isa_decoder(
name = "riscv32i_isa",
src = "riscv32i.isa",
includes = [],
isa_name = "RiscV32I",
deps = [
"//riscv_semantic_functions:riscv32i",
],
)
Essa regra chama a ferramenta e o gerador de analisador de ISA para gerar o código C++,
em seguida compila o gerado em uma biblioteca que outras regras podem usar
usando o rótulo //riscv_isa_decoder:riscv32i_isa
. A seção includes
é usada
para especificar outros arquivos .isa
que o arquivo de origem pode incluir. O
isa_name
é usado para especificar qual isa específica, necessária se mais de uma for
especificada, no arquivo de origem para gerar o decodificador.
Adicionar instruções de ALU de registro-registro
Agora é hora de adicionar algumas novas instruções ao arquivo riscv32i.isa
. O primeiro
grupo de instruções são instruções ALU de registro-registro, como add
,
and
etc. No RiscV32, todas elas usam o formato de instrução binária do tipo R:
31 a 25 | 24 a 20 | 19..15 | 14 a 12 | 11..7 | 6 a 0 |
---|---|---|---|---|---|
7 | 5 | 5 | 3 | 5 | 7 |
func7 | rs2 | rs1 | func3 | º | código de operação |
Embora o arquivo .isa
seja usado para gerar um decodificador independente de formato, ainda é
útil considerar o formato binário e o layout dele para orientar as entradas. Como
você pode ver, há três campos relevantes para o decodificador que preenchem os
objetos de instrução: rs2
, rs1
e rd
. Neste ponto, vamos escolher usar
esses nomes para registros inteiros codificados da mesma maneira (sequências de bits),
nos mesmos campos de instrução, em todas as instruções.
As instruções que vamos adicionar são as seguintes:
add
: adição de número inteiro.and
: bit a bit e.or
: OR bit a bit.sll
: desloca para a esquerda lógico.sltu
: definido como menor que, sem assinatura.sub
: subtração de números inteiros.xor
: XOR bit a bit.
Cada uma dessas instruções será adicionada à seção opcodes
da definição do slot riscv32i
. Lembre-se de que precisamos especificar o nome, os códigos de operação,
o desmontagem e a função semântica de cada instrução. O nome é fácil,
vamos usar apenas os nomes dos códigos de operação acima. Além disso, todos usam os mesmos operandos. Portanto,
podemos usar { : rs1, rs2 : rd}
para a especificação de operando. Isso significa que
o operando de origem do registro especificado por rs1 terá o índice 0 no vetor de operando
de origem no objeto de instrução, o operando de origem do registro especificado
por rs2 terá o índice 1, e o operando de destino do registro especificado por rd
será o único elemento no vetor de operando de destino (no índice 0).
A próxima etapa é a especificação da função semântica. Isso é feito usando a palavra-chave
semfunc
e uma string C++ que especifica uma função chamável que pode ser usada para atribuir
a um std::function
. Neste tutorial, vamos usar funções, então a string
chamável será "&MyFunctionName"
. Usando o esquema de nomenclatura sugerido pela
instrução fence
, eles devem ser "&RV32IAdd"
, "&RV32IAnd"
etc.
Por último, a especificação de desmontagem. Ela começa com a palavra-chave disasm
e
é seguida por uma lista de strings separadas por vírgulas que especifica como a
instrução deve ser impressa como uma string. O uso de um sinal %
na frente de um nome de operando indica uma substituição de string usando a representação de string desse operando. Para a instrução add
, isso seria: disasm: "add", "%rd,
%rs1,%rs2"
. Isso significa que a entrada da instrução add
será parecida com esta:
add{ : rs1, rs2 : rd},
semfunc: "&RV32IAdd",
disasm: "add", "%rd, %rs1, %rs2";
Edite o arquivo riscv32i.isa
e adicione todas essas instruções à
descrição .isa
. Se você precisar de ajuda ou quiser verificar seu trabalho, o arquivo de descrição completa está aqui.
Depois que as instruções forem adicionadas ao arquivo riscv32i.isa
, será necessário
adicionar declarações de função para cada uma das novas funções semânticas que foram
referenciadas ao arquivo rv32i_instructions.h
localizado em
`../semantic_functions/. Novamente, se você precisar de ajuda (ou quiser verificar seu trabalho),
a resposta está
aqui.
Depois disso, volte para o diretório riscv_isa_decoder
e faça a reconstrução. Fique à vontade para examinar os arquivos de origem gerados.
Adicionar instruções de ALU com ações imediatas
O próximo conjunto de instruções que 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 a 20 | 19 a 15 | 14 a 12 | 11..7 | 6..0 |
---|---|---|---|---|
12 | 5 | 3 | 5 | 7 |
imm12 | rs1 | func3 | º | código de operação |
O formato imediato especializado do tipo I:
31 a 25 | 24 a 20 | 19..15 | 14 a 12 | 11..7 | 6 a 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 | º | código de operação |
Como você pode ver, os nomes de operandos rs1
e rd
se referem aos mesmos campos de bits
anteriores e são usados para representar registros de números inteiros. Portanto, esses nomes podem ser
retidos. Os campos de valor imediato têm comprimento e localização diferentes, e
dois deles (uimm5
e uimm20
) não têm assinatura, enquanto imm12
tem assinatura. Cada um deles
usa o próprio nome.
Portanto, os operandos das instruções do tipo I precisam ser { : rs1, imm12 :rd
}
. Para as instruções especializadas do tipo I, deve ser { : rs1, uimm5 : rd}
.
A especificação do operando da instrução do tipo U precisa ser { : uimm20 : rd }
.
As instruções do tipo I que precisamos adicionar são:
addi
: adicione imediatamente.andi
: bit a bit e com imediato.ori
: bit a bit ou com imediato.xori
: XOR bit a bit com imediato.
As instruções I-Type especializadas que precisamos adicionar são:
slli
: deslocamento lógico para a esquerda por imediato.srai
: desloca a aritmética da direita por imediato.srli
: desloca a lógica à direita imediatamente.
As instruções do tipo u que precisamos adicionar são:
auipc
: adiciona o valor superior imediato ao pc.lui
: carregar imediatamente superior.
Os nomes a serem usados para os opcodes seguem naturalmente dos nomes de instrução
acima (não é necessário criar novos, eles são todos exclusivos). Quando se trata de
especificar as funções semânticas, lembre-se de que os objetos de instrução codificam
interfaces para os operandos de origem que são agnósticos em relação ao tipo de operando
subjacente. Isso significa que instruções com a mesma operação, mas
que podem diferir nos tipos de operando, podem compartilhar a mesma função semântica. Por exemplo,
a instrução addi
executa a mesma operação que a instrução add
se
ignorar o tipo de operando, para que possa usar a mesma especificação de função
semântica "&RV32IAdd"
. O mesmo ocorreu para andi
, ori
, xori
e slli
.
As outras instruções usam novas funções semânticas, mas precisam ser nomeadas
com base na operação, não em operandos. Portanto, para srai
, use "&RV32ISra"
. As
instruções do tipo U-auipc
e lui
não têm registros equivalentes. Portanto, use "&RV32IAuipc"
e "&RV32ILui"
.
As strings de desmontagem são muito semelhantes às do exercício anterior, mas
como esperado, as referências a %rs2
são substituídas por %imm12
, %uimm5
ou %uimm20
, conforme apropriado.
Vá em frente, faça as alterações e crie. Confira a saída gerada. Assim como antes, você pode verificar seu trabalho em relação a riscv32i.isa e a rv32i_instructions.h.
Adicionar instruções de ramificação e link direto
As instruções de ramificação e de salto e vinculação que precisamos adicionar usam um operando de destino
que é apenas implícito na própria instrução, ou seja, o próximo valor de
pc. Nesta etapa, vamos tratar isso como um operando adequado com o nome
next_pc
. Ela será definida em um tutorial posterior.
Instruções da ramificação
As ramificações que estamos adicionando usam a codificação tipo B.
31 | 30,25 | 24 a 20 | 19..15 | 14..12 | 11 a 8 | 7 | 6..0 |
---|---|---|---|---|---|---|---|
1 | 6 | 5 | 5 | 3 | 4 | 1 | 7 |
imm | imm | rs2 | rs1 | func3 | imm | imm | código de operação |
Os diferentes campos imediatos são concatenados em um valor imediato assinado de 12 bits. Como o formato não é realmente relevante, vamos chamar esse bimm12
imediato, para a ramificação imediata de 12 bits. A fragmentação será abordada no próximo tutorial sobre a criação do decodificador binário. Todas as instruções de ramificação comparam os registros inteiros especificados por rs1 e rs2. Se a condição for verdadeira, o valor imediato será adicionado ao valor de pc atual para produzir o endereço da próxima instrução a ser executada. Os operandos das
instruções de ramificação precisam ser { : rs1, rs2, bimm12 : next_pc }
.
As instruções de ramificação que precisamos adicionar são:
beq
: ramificação, se 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.
Esses nomes de opcode são todos exclusivos, então podem ser reutilizados na descrição
.isa
. Obviamente, novos nomes de função semântica precisam ser adicionados, por exemplo,
"&RV32IBeq"
etc.
A especificação de desmontagem agora é um pouco mais complicada, já que o endereço da
instrução é usado para calcular o destino, sem que ele faça parte
dos operandos de instrução. No entanto, como faz parte das informações armazenadas no objeto de instrução, estão disponíveis. A solução é usar a
sintaxe de expressão na string desmontagem. Em vez de usar "%" seguido pelo
nome do operando, digite %(expressão: formato de impressão). Somente expressões muito simples são aceitas, mas o endereço mais o deslocamento é uma delas, com o símbolo @
usado para o endereço de instrução atual. O formato de impressão é semelhante aos
formatos printf do estilo C, mas sem o %
inicial. O formato de desmontagem da
instrução beq
fica assim:
disasm: "beq", "%rs1, %rs2, %(@+bimm12:08x)"
Instruções de salto e vinculação
Apenas duas instruções de salto e vinculação precisam ser adicionadas, jal
(salto e vinculação) e
jalr
(salto e vinculação indiretos).
A instrução jal
usa a codificação do tipo J:
31 | 30 a 21 | 20 | 19..12 | 11 a 7 | 6..0 |
---|---|---|---|---|---|
1 | 10 | 1 | 8 | 5 | 7 |
imm | imm | imm | imm | a | código de operação |
Assim como nas instruções de ramificação, o imediato de 20 bits é fragmentado em
vários campos. Por isso, vamos nomeá-lo como jimm20
. A fragmentação não é importante
no momento, mas será abordada no próximo
tutorial sobre a criação do decodificador binário. A especificação
do operando se torna { : jimm20 : next_pc, rd }
. Há dois operandos de destino, o próximo valor de pc e o registro de link especificado na instrução.
Semelhante às instruções de ramificação acima, o formato de desmontagem fica assim:
disasm: "jal", "%rd, %(@+jimm20:08x)"
O salto indireto e o link indireto usam o formato I-Type com o imediato de 12 bits. Ele
adiciona o valor imediato com extensão de sinal ao registro de inteiro especificado por
rs1
para produzir o endereço de instrução de destino. O registro de links é o
registro de números inteiros especificado por rd
.
31 a 20 | 19 a 15 | 14 a 12 | 11..7 | 6..0 |
---|---|---|---|---|
12 | 5 | 3 | 5 | 7 |
imm12 | rs1 | func3 | º | código de operação |
Se você viu o padrão, agora deduza que a especificação do operando
para jalr
precisa ser { : rs1, imm12 : next_pc, rd }
e a especificação
de desmontagem:
disasm: "jalr", "%rd, %rs1, %imm12"
Faça as alterações e crie. Confira a saída gerada. Assim como antes, você pode verificar seu trabalho em relação a riscv32i.isa e rv32i_instructions.h.
Adicionar instruções da loja
As instruções para a loja são muito simples. Todos eles usam o formato S-Type:
31 a 25 | 24 a 20 | 19..15 | 14 a 12 | 11..7 | 6 a 0 |
---|---|---|---|---|---|
7 | 5 | 5 | 3 | 5 | 7 |
imm | rs2 | rs1 | func3 | imm | código de operação |
Como você pode ver, esse é mais outro caso de um imediato fragmentado de 12 bits. Vamos
chamá-lo de simm12
. Todas as instruções de armazenamento armazenam o valor do registro inteiro especificado por rs2 no endereço efetivo na memória recebido adicionando o valor do registro inteiro especificado por rs1 ao valor estendido de sinal do imediato de 12 bits. O formato do operando precisa ser { : rs1, simm12, rs2 }
para
todas as instruções de armazenamento.
As instruções da loja que precisam ser implementadas são:
sb
: byte de armazenamento.sh
: armazena metade da palavra.sw
: palavra da loja.
A especificação de desmontagem para sb
é a esperada:
disasm: "sb", "%rs2, %simm12(%rs1)"
As especificações da função semântica também são o que você espera: "&RV32ISb"
etc.
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.isa e rv32i_instructions.h.
Adicionar instruções de carregamento
As instruções de carga são modeladas de maneira um pouco diferente das outras instruções no simulador. Para modelar casos em que a latência de carregamento é incerta, as instruções de carregamento são divididas em duas ações separadas: 1) cálculo de endereço eficaz e acesso à memória e 2) write-back do resultado. No simulador, isso é feito dividindo a ação semântica da carga em duas instruções separadas, a instrução principal e uma instrução filha. Além disso, quando especificamos operandos, precisamos especificá-los para a instrução principal e filha. Isso é feito tratando a especificação do operando como uma lista de triplos. A sintaxe é:
{(predicate : sources : destinations),
(predicate : sources : destinations), ... }
Todas as instruções de carregamento usam o formato I-Type, assim como muitas das instruções anteriores:
31..20 | 19 a 15 | 14 a 12 | 11..7 | 6..0 |
---|---|---|---|---|
12 | 5 | 3 | 5 | 7 |
imm12 | rs1 | func3 | º | código de operação |
A especificação de operandos divide os operandos necessários para calcular o endereço
e iniciar o acesso à memória do destino do registro para carregar dados:
{( : rs1, imm12 : ), ( : : rd) }
.
Como a ação semântica é dividida em duas instruções, as funções semânticas
precisam especificar dois elementos chamáveis da mesma forma. Para lw
(palavra de carga), seria
escrito:
semfunc: "&RV32ILw", "&RV32ILwChild"
A especificação de desmontagem é mais convencional. Nenhuma menção é feita
à instrução filha. Para lw
, ele será:
disasm: "lw", "%rd, %imm12(%rs1)"
As instruções de carga que precisam ser implementadas são:
lb
: byte de carga.lbu
: carrega o byte não assinado.lh
: carrega a metade da palavra.lhu
: carrega meia palavra sem assinatura.lw
: carregar palavra.
Faça as alterações e crie. Confira a saída gerada. Assim como antes, você pode verificar seu trabalho em relação a riscv32i.isa e rv32i_instructions.h.
Obrigado por chegar até aqui. Esperamos que essas informações tenham sido úteis.