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