# Video-chamada Spike
- [1. Modificar Spike](#1-Modificar-Spike)
- [1.1. Adicionar Instruções](#11-Adicionar-instruções)
- [1.2. Acrescentar funcionalidades](#12-Acrescentar-funcionalidades)
- [1.3. Explicação do funcionamento das instruções](#13-Explicação-do-funcionamento-das-instruções)
- [2. Recomendações](#2-Recomendações)
- [#2.1. Ambiente de desenvolvimento](#21-Ambiente-de-desenvolvimento)
- [#2.2. Arquitetura Software](#22-Arquitetura-Software)
- [#2.3. Extras](#23-Extras)
- [3. Compilação das ferramentas](#3-Observações-durante-compilação-das-ferramentas)
- [3.1. Compilador](#31-Compilador)
- [3.2. Proxy-Kernel](#32-Proxy-Kernel)
- [3.3. Spike](#33-Spike)
- [3.4. Encodings.h](#34-Encodingsh)
- [4. Anexos](#4-Anexos)
- [4.1. Compilar Spike](#41-Compilar-Spike)
- [4.2. Binário de teste](#42-Binário-de-teste)
- [4.3. Instrução ss.ld.w modificada](#43-Instrução-ssldw-modificada)
# 1. Modificar Spike
- [Spike modificado na minha dissertação, no branch `uve`.](https://github.com/MiguelPedrosa/riscv-isa-sim/tree/a3507fe124c2727e785eacd03589a87622d1a3f5)
- [Alterações feitas durante a minha dissertação.](https://github.com/MiguelPedrosa/riscv-isa-sim/compare/55f664b6b712fbe351236c73936309bd9c60a5d1..a3507fe124c2727e785eacd03589a87622d1a3f5)
- Ficheiros na pasta `UVE-Test-Impl` serviram somente para teste e não são compilados com o Spike.
- [Instruções usadas nos benchmarks.](https://github.com/MiguelPedrosa/riscv-isa-sim/blob/a3507fe124c2727e785eacd03589a87622d1a3f5/UVE-instruction-usage.md)
- Para criar um comportamento novo, o melhor é procurar instruções semelhantes já existentes para descobrir como o fazem. As instruções das extensões `F` e `D` são úteis para descobrir como mexer com números em virgula flutuante e a extensão `V` tem os registos contruídos de forma semelhante aos do UVE.
- É difícil de navegar no projeto porque existem muitas macros e substituições de texto. É raro conseguir saltar para a definição/implementação duma função, por isso uma boa ferramenta de pesquisa de texto será o mais útil para navegar no projeto.
## 1.1. Adicionar instruções
- Adicionar em `encoding.h` a informação da instrução. [Versão com todas instruções UVE](https://gist.github.com/MiguelPedrosa/89f003150f513fd5a5aa6b32131728c8#file-encoding-h).
- Criar ficheiro em `riscv/insns/<ins-name>.h`, onde os pontos (`.`) do nome da instrução são substituídos por underscores (`_`).
- Acrescentar instrução em `riscv/riscv.mk.in`.
- Para testar, criei a instrução `ss.ld.w` e incluí o código na [secção 4.3](#43-Instrução-ssldw-modificada).
## 1.2 Acrescentar funcionalidades
Numa instrução, para aceder aos valores passados pelos registos é preciso primeiro descobrir qual o registo que a instrução invocada usou (u0-u31, x0-x31, etc.) e depois extrair o valor que esse registo contém.
1. Como o UVE acrescenta registos novos, temos de acrescentar algumas funções na classe `insn_t`, do ficheiro `riscv/decode.h`. Na prática, muitas das funções que incluí a seguir fazem algo idêntico ao que já existe, mas acrescentar novas cria mais flexibilidade caso a encodificação das instruções do UVE mude.
```c
// Registers for configuration instructions
int64_t uve_conf_destination() { return x(7, 5); }
int64_t uve_conf_base() { return x(15, 5); }
int64_t uve_conf_size() { return x(20, 5); }
int64_t uve_conf_stride() { return x(27, 5); }
// Registers for computation instructions
int64_t uve_comp_dest() { return x(7, 5); }
int64_t uve_comp_src1() { return x(15, 5); }
int64_t uve_comp_src2() { return x(20, 5); }
// Registers for branching instructions
int64_t uve_branch_reg() { return x(15, 5); }
// Calculate offset for UVE branching instruction
int64_t uve_branch_imm() { return (x(8, 4) << 1) + (x(22, 6) << 5) + (x(7, 1) << 11) + (imm_sign() << 12); }
```
- Se for preciso consultar o encoding das instruções e não houver uma versão mais atualizada, dá para o fazer [nesta tabela](https://docs.google.com/spreadsheets/d/1884rlTVR9CgEPJ85GKEnYcThA31_4Doyuy6GYNgTQYk/edit).
- Explicação da função/macro `x`: O primeiro argumento é o bit onde começa a informação que queremos descobrir. O segundo argumento é a quantidade de bits que temos de ler.
- Exemplo: Na função `uve_conf_destination`, queremos descobrir qual o registo em que deve ser programada uma stream: vendo o [encoding das instruções de configuração](https://docs.google.com/spreadsheets/d/1884rlTVR9CgEPJ85GKEnYcThA31_4Doyuy6GYNgTQYk/edit#gid=1027939292&range=B3:AG3), este registo começa no bit 7 e acaba no 11, portanto começa em 7 e requere 5 bits para computar.
2. No ficheiro duma instrução UVE (como `riscv/insns/ss_ld_w.h`), podemos aceder às funções de decode, usando o objeto `insn`. Acesso a registos nativo do RISC-V pode ser feito com a macro `READ_REG`. Acesso a registos UVE ainda não existe e tem de ser acrescentado como parte deste trabalho. Para aceder à memória do binário em execussão, usa-se a função `MMU.load<uint32>` e semelhantes.
3. (Sem certezas, não testei recentemente isto) Para acrescentar registos ou funcionalidades parecidas, o que fiz durante a dissertação foi criar uma classe que representasse todos os objetos, à semelhança do que é feito para a [extensão V](https://github.com/riscv-software-src/riscv-isa-sim/blob/32742924154a41e457f5b64e745485a3ebcd0daf/riscv/processor.h#L337). Qualquer ficheiro .h ou .cpp novo, tem de ser adicionado ao `riscv.mk.in`, nas variáveis [`riscv_install_hdrs`](https://github.com/riscv-software-src/riscv-isa-sim/blob/32742924154a41e457f5b64e745485a3ebcd0daf/riscv/riscv.mk.in#L16) e [`riscv_srcs`](https://github.com/riscv-software-src/riscv-isa-sim/blob/32742924154a41e457f5b64e745485a3ebcd0daf/riscv/riscv.mk.in#L45).
### 1.2.1. Instrução branch
- A função `uve_branch_imm` não retorna um índice de registos, mas em vez um valor imediato. As contas para calcular este valor foram extraídas desta [linha da tabela de encoding](https://docs.google.com/spreadsheets/d/1884rlTVR9CgEPJ85GKEnYcThA31_4Doyuy6GYNgTQYk/edit#gid=1314391799&range=E5:AG5).
- O imediato será um valor com 12 bits mais sinal: bit 0 ao bit 11. Nas instruções de branch da tabela de encoding, os valores entre parêntesis retos indicam os bits de índice do valor imediato. É sempre preciso ter em conta o bit 0 e não está representado porque será sempre implicitamente 0 por filosofia do RISC-V. É por causas do bit 0 que os cálculos estão feito como `x(8, 4) << 1` e não como `x(8, 4)`.
- O [bit 12](https://docs.google.com/spreadsheets/d/1884rlTVR9CgEPJ85GKEnYcThA31_4Doyuy6GYNgTQYk/edit#gid=1314391799&range=E5) representa o sinal do imediato, ou seja, se fazemos um salto na execussão para a frente ou para trás.
- (Não testado recentemente) Para realizar um salto, deve ser preciso fazer algo como `set_pc(pc + branchIMM)`. Só implementei [um salto](https://github.com/MiguelPedrosa/riscv-isa-sim/blob/a3507fe124c2727e785eacd03589a87622d1a3f5/riscv/insns/so_b_nc.h#L11) durante a dissertação.
- Mais de metade do meu esforço a modificar o Spike foi nesta instrução :upside_down_face:.
## 1.3. Explicação do funcionamento das instruções
Durante a compilação do Spike, será criada uma cópia do ficheiro [riscv/insn_template.cc](https://github.com/riscv-software-src/riscv-isa-sim/blob/32742924154a41e457f5b64e745485a3ebcd0daf/riscv/insn_template.cc) por cada instrução. Neste processo, o nome da instrução substitui o atributo `NAME` no template e o código de cada instrução substitui o atributo `#include "insns/NAME.h"`. Isto é uma forma "ágil" de criar várias versões da mesma instrução (32/64bits, com/sem logs, etc.), mas tem implicações no desenvolvimento:
* Não podemos adicionar includes por instrução. Para incluír um header novo, é preciso modificar o ficheiro [`riscv/insn_template.h`](https://github.com/riscv-software-src/riscv-isa-sim/blob/32742924154a41e457f5b64e745485a3ebcd0daf/riscv/insn_template.h) e o novo header vai passar a ser incluído em todas as instruções.
* Temos suporte mínimos de IDEs. Estamos a fazer código que será colocado dentro duma função, mas os IDEs acreditam que estamos a criar código no espaço global.
* Temos acceso às variáveis `processor_t* p`, `insn_t insn` e `reg_t pc`, dado estas serem os argumentos da função. `pc` é o `process counter` e é útil para instruções que façam saltos. `insn` é a instrução que foi invocada (incluíndo valores dos registos) e até agora só vi a ser usada com funções semelhantes à [secção 1.2](#12-Acrescentar-funcionalidades).
# 2. Recomendações
## 2.1. Ambiente de desenvolvimento
- No final do trabalho, o build server não me serviu de muito, pois passei a fazer tudo localmente.
- Em Windows, dá para instalar todas as ferramentas no WSL e fazer o desenvolvimento por aí.
- É possível não compilar extensões no Spike que não usadas para poupar tempos de compilação. Eu comentei as extensões `q`, `k`, `v`, `zce` e `p`. Talvez seja possível ocultar mais, mas não testei o suficiente.
- Usar flag `-j$(nproc)` durante compilação do Spike para acelerar processo. Flag `-B` para forçar compilação, é útil porque alterações nos headers não causam uma recompilação.
- Por vezes o comando `objdump -S a.out` dá jeito para rever o assembly gerado pelo compilador. É preciso usar `$RISCV/bin/riscv64-unknown-elf-objdump`.
- Symlinks para o gcc, Spike e proxy-kernel simplificam bastante a invocação de comandos.
## 2.2. Arquitetura Software
- Ter em conta desde início a instrução `ss.cfg.vec`. Muitas instruções vão depender da existência desta flag e condiciona a geração de valores da streaming engine.
- Uso de predicados cria 'lacunas' de valores nos registos vetoriais. Talvez nos de predicados também.
## 2.3. Extras
- O livro ["The RISC-V Reader: An Open Architecture Atlas"](http://www.riscvbook.com/) ajuda a entender muito do que acontece no RISC-V e algumas das extensões. A maioria das versões é paga, mas existe uma [versão em português (BR)](http://www.riscvbook.com/portuguese/) gratuita no mesmo site.
# 3. Observações durante compilação das ferramentas
Nota: `$RISCV` é uma variável para um diretório. Isto é algo desatualizado, mas ainda é usado nos repositórios destas ferramentas. Dantes, as ferramentas procuravam nesta localização utilitários para fazer compilações e desenvolvimento. Agora é usado como um lugar de destino da instalação das várias ferramentas.
## 3.1. Compilador
- O nome onde do repositório do compilador deve ser "riscv-opcodes". Os scripts de python assumem que é este o nome do projeto, ainda que não seja o nome do repositório no gitlab.
- O QEMU e uma dependência sua têm o URL errado nos módulos. Só é possível corrigir isto após o repositório do QEMU ser descarregado. Primeiro correr o comando `git submodule update --init --recursive` (deve falhar só a descarregar o QEMU), de seguida editar o ficheiro `ext_modules/riscv-gnu-toolchain/.gitmodules` com o [novo URL do QEMU](https://github.com/riscvarchive/riscv-qemu) e depois correr os comandos `git submodule sync --recursive` e `git submodule update --init --recursive`.
- A dependência do QEMU, `qemu-palcode`, vai ter o mesmo problema. É preciso editar `ext_modules/riscv-gnu-toolchain/riscv-qemu/.gitmodules` para que o `palcode` referencia o [novo URL](https://github.com/rth7680/qemu-palcode) (Não encontrei diferença, mas parece que existe) e correr os comandos `git submodule sync --recursive` e `git submodule update --init --recursive`.
- O compilador de UVE é baseado no `gcc8.2` e depende duma package chamada `isl`. Para esta versão do `gcc` funcionar com uma versão moderna de `isl`, é preciso incluir as seguintes linhas no ficheiro `riscv-opcodes/ext_modules/riscv-gnu-toolchain/riscv-gcc/gcc/graphite.h`:
```c
#include <isl/id.h>
#include <isl/space.h>
```
- Notas:
- Eu editei o `configure.sh` para modificar o `INSTALL_DIR` pois queria que o compilador e outras utilidades fossem instalados num diretório diferente.
- A compilação do `gcc` é feita por `stages`, dado ser um cross-compiler. Isto significa que é preciso correr mais que uma vez o `make -j$(nproc)` e `make linux -j$(nproc)` conforme aparece no ficheiro `configure.sh`. Não consegui que uma execussão normal deste script gerasse o compilador, por isso fui comentando as linhas `make linux -j$(nproc)` e `make linux -j$(nproc)` e repetindo a execussão do ficheiro até que os outputs parecessem repetido.
- O compilador final encontra-se em `$RISCV/bin/riscv64-unknown-elf-gcc`.
## 3.2. Proxy-Kernel
O único comando diferente foi o do `configure` que exigiu referência explícita para o `CC` e `OBJCOPY`.
```bash
$ CC=$RISCV/bin/riscv64-unknown-elf-gcc \
OBJCOPY=$RISCV/bin/riscv64-unknown-elf-objcopy \
../configure --prefix=$RISCV --host=riscv64-unknown-elf
```
- Notas:
- O proxy-kernel final encontra-se em `$RISCV/riscv64-unknown-elf/bin/pk`.
## 3.3. Spike
- Não tive dificuldades em compilar o Spike diretamente do repositório.
- Só quando adicionei um encoding mais recente com as instruções UVE tive dificuldades em compilar, mas isso é discutido na secção seguinte.
## 3.4. Encodings.h
- Ao acrescentar as instruções do UVE tive problemas pois o ficheiro `encoding.h` que tinha esta desatualizado.
- Gerei uma nova versão a partir do [projeto oficial](https://github.com/riscv/riscv-opcodes) com as instruções UVE, mas o Spike estava a contar com instruções mais desatualizadas, por isso reverti para um [commit mais antigo dos opcodes](https://github.com/riscv/riscv-opcodes/tree/8df0274582cedd9070bb92f6e7d9e225de005f1a).
- Gerei novamente o ficheiro e o resultado final encontra-se [num gist público](https://gist.github.com/MiguelPedrosa/89f003150f513fd5a5aa6b32131728c8#file-encoding-h).
- Para gerar o ficheiro, fui a uma pasta gerada pelo setup da compilação do compilador. Após correr o script `inject_uve.sh`, surge uma pasta chamada `build/templates/`. Os ficheiros desta pasta devem ser copiados para o repositório dos opcodes com o nome modificado para `rv_uve_...`.
- Acredito que o ficheiro `m5ops.expanded` não precisa de ser copiado. Do meu entendimento, eram instruções para gerar logs quando um binário corria no gem5. Para correr no Spike não me parecem necessárias.
- Depois é preciso modificar o ficheiro `constants.py` para este compreender os registos do UVE:
```python
# Unlimited Vector Extension
#arglut['vd'] = (11,7) # Same as vector
#arglut['vs1'] = (19,15) # Same as vector
#arglut['vs2'] = (24,20) # Same as vector
#arglut['rs1'] = (19,15) # Same as RV32I
#arglut['rs2'] = (24,20) # Same as RV32I
#arglut['rs3'] = (31,27) # Same as RV32I
#arglut['rd'] = (11,7) # Same as RV32I
arg_lut['pd'] = (10,7)
arg_lut['ps1'] = (18,15)
arg_lut['ps2'] = (22,20)
arg_lut['ps3'] = (27,25)
arg_lut['imm12aHi'] = (28,22)
arg_lut['imm12aLo'] = (11,7)
```
- Seguindo as instruções do repositório, é gerado um ficheiro com o nome `encodings.out.h`. É este que deve ser copiado para o Spike.
# 4. Anexos
Ficheiros adicionais produzidos.
## 4.1. Compilar Spike
Como o rebuild do Spike era frequente, escrevi este script para facilitar o processo. O passo de `make install` exigiu-me permissões, por isso o script foi sempre corrido com `sudo`. Sem argumentos, o script faz só `make && make install`. Com o argumento `full`, é forçada uma compilação completa. Isto é útil porque alterações nos headers (excluíndo as instruções) não são detetadas pelo make, por isso forçamos a compilação destes. Com o argumento `all` faz o mesmo que o `full`, mas corre novamente o script `./configure`. Os paths devem ser modificados.
```bash
#!/bin/bash
SPIKE_DIR=/home/miguel/Projects/UVE/sources/spike
RISCV=/home/miguel/Projects/UVE/tools
# Move to spike' source directory and store current directory
pushd $SPIKE_DIR
# If required, empty the build directory
[ "$1" = "all" ] && rm -rdf build && mkdir build
cd build
# If required, run the configure command
[ "$1" = "all" ] && ../configure --prefix="$RISCV"
# The "full" argument adds the additional "-B" argument to make, that forces
# a full re-compilation. We don't need to test for "all" as all previous files
# should have been deleted early in the script. Also, we're only interested in
# installing if build is succesfull
make -j$(nproc) $([ "$1" = "full" ] && echo "-B") && make install
# Return to original directory
popd
```
## 4.2. Binário de teste
Testei com o seguinte binário a execussão da instrução `ss.ld.w`. Como a instrução que escolhi usa um tamanho de words, preciso que o array seja dum tipo com mesmo tamanho: `int32_t`.
```c
#include <stdio.h>
int main()
{
#define SIZE 10
int32_t array[SIZE];
for (int32_t i = 0; i < SIZE; i++) {
array[i] = i * 10;
}
asm volatile("ss.ld.w u9, %[offset], %[size], %[stride]\n\t"
:: [offset] "r" (array), [size] "r" (SIZE), [stride] "r" (1)
: "memory");
}
```
**IMPORTANTE**: No código acima, o compilador não consegue observar alterações no array, por isso acredita que é inútil modificar este e não gera as instruções de atribuição do loop. Podemos força-lo a escrevê-las de uma ou mais formas:
- 1. Usando a diretiva "memory" no assembly. Isto força um flush da memória modificada até àquele instante.
- 2. Declarando o array como `volatile` para indicar que pode ser modificado de formas não previstas.
## 4.3 Instrução `ss.ld.w` modificada
Implementei esta instrução para testar acessos aos índices dos registos invocados, valores dos registos RISC-V e acesso a memória. De momento, a instrução faz um print de todos os valores que a stream deverá gerar.
```c
/* This UVE instruction (ss.ld.w) uses 4 registers: 1 UVE vector register and
* 3 RISC-V native registers. The READ_REG function is used to read the
* contents of the native RISC-V registers. We would need to use a different
* function to access a floating-point register. */
fprintf(stderr, "Start of logs inside ss.ld.w instruction\n");
/* Find all indexes of the used registers */
auto streamReg = insn.uve_conf_destination();
auto baseReg = insn.uve_conf_base();
auto sizeReg = insn.uve_conf_size();
auto strideReg = insn.uve_conf_stride();
/* Find the values that each register holds. Base has a different type so it can
* points to an address. The UVE registers are not implemented, so they cannot
* have any value to read. */
reg_t base = READ_REG(baseReg);
int32_t size = READ_REG(sizeReg);
int32_t stride = READ_REG(strideReg);
/* For debug, print the indexes of each register as well as their contents */
fprintf(stderr, "UVE Register u%ld\n", streamReg);
fprintf(stderr, "RISC-V Register x%2ld: %p (Base)\n", baseReg, (void*) base);
fprintf(stderr, "RISC-V Register x%2ld: %d (Size)\n", sizeReg, size);
fprintf(stderr, "RISC-V Register x%2ld: %d (Stride)\n", strideReg, stride);
/* For testing, print all the values the configuration should generate */
for (int32_t i = 0; i < size; i++) {
/* Find where the next element to print should be */
int32_t offset = i * stride;
/* Stride does not considere byte sizes, so we need to adjust the offset */
int32_t byteOffset = offset * sizeof(std::int32_t);
/* Read from memory our value stored in the location given as an argument */
int32_t value = MMU.load<int32_t>(base + byteOffset);
fprintf(stderr, "array[%d]: %d\n", offset, value);
}
fprintf(stderr, "End of logs inside ss.ld.w instruction\n");
```