Sobre Virgule e seu simulador emulsiV

Guillaume Savaton, ESEO
Traduzido por Carlos Delfino.

emulsiV é um simulador virtual para um processador RISC simples, chamado Virgule.

Virgule é um processador de núcleo RISC de 32-bits que implementa um conjunto mínimo de instruções RISC-V. Aqui, “mínimo” significa que Virgule aceita somente as instruções que o compilador C deve gerar a partir de um programa "C stand-alone".

Motivativação e Escopo

Virgule e emulsiV são usados para ensinar arquitetura e projeto de circuitos digitais para iniciante em ESEO. Antes de escolher uma arquitetura de processador, nos tivemos os seguintes requisitos em mente:

Dentre as arquiteturas candidatas, o RISC-V preencheu todos os requisitos:

Estas propriedades permite diversos cenários de ensino.

Em um curso de arquitetura de computadores, estudantes descobrem como um processador funciona, que tipo de linguagem e ferramentas podem ser usadas para criar um programa em baixo nível (Assembly, C). Um simulador é a melhor forma de visualizar como cada instrução afeta o caminho dos dados.

Em um curso de projeto de circuitos digitais, estudantes podem implementar um core de processador em VHDL ou Verilog, ou instânciar um para fazer seu próprio system-on-chip.

Arquitetura do Processador

Relação com a especificação RISC-V

Virgule implementa todas as instruções computacionais, de transferência de controle, e acesso a memória do subconjunto de inteiros do RV32I da especificação RISC-V. Ele também fornece uma instrução de retorno de exceção que pode ser usada em funções de tratamento de interrupção.

As seguintes intruções não estão disponíveis:

A arquitetura RISC-V define três níveis de privilégios: user, supervisor, e machine. Virgule somente suporta o nível machine.

Registradores

Virgule contem os seguintes registradores 32-bit:

Organização da memória

Formato do dado para instruções de carga e gravação seguem as seguintes convenções:

Nome Tamanho do dados em (bits) O enderço é múltiplo de
Byte (B) 8 1
Half word (H) 16 2
Word (W) 32 4

Em memória, 16-bit e 32-bit os dados seguem a ordenção little-endian.

Os dois endereços a seguir tem uma regra específica:

Interrupções

O Virgule implementa um hardware simples para o esquema de interrupção no modo previlegiado de máquina. Não controle de interrupção ou registrador de estatus internamente no core de processador.

Quando ele recebe uma requisição de interrupção, Virgule executa as seguintes operações:

  1. Completa a intrução corrente.
  2. Chaveia para um estado de interrupção.
  3. Define mepc para o endereço da próxima instrução.
  4. Define pc para 4, que transferirá o controle para a função de tratamento de interrupção.

Retornando de uma função de tratamento de interrupção é feito com a instrução mret. Esta intrução tem o seguinte efeito:

  1. Copia mepc para pc.
  2. Chaveia para um estado de interrompível.

Conjunto de Instruções

Na seguinte tabela:

Instrução Sintáxe Operação
Load Upper Immediate LUI rd, imm rd ← imm
Add Upper Immediate to PC AUIPC rd, imm rd ← pc + imm
Jump And Link JAL rd, imm rd ← pc + 4; pc ← pc + imm
Jump And Link Register JALR rd, rs1, imm rd ← pc + 4; pc ← rs1 + imm
Branch if Equal BEQ rs1, rs2, imm
if rs1 = rs2:
    pc ← pc + imm
else:
    pc ← pc + 4
Branch if Not Equal BNE rs1, rs2, imm
if rs1 ≠ rs2:
    pc ← pc + imm
else:
    pc ← pc + 4
Branch if Less Than BLT rs1, rs2, imm
if signed(rs1) < signed(rs2):
    pc ← pc + imm
else:
    pc ← pc + 4
Branch if Greater or Equal BGE rs1, rs2, imm
if signed(rs1) ≥ signed(rs2):
    pc ← pc + imm
else:
    pc ← pc + 4
Branch if Less Than Unsigned BLTU rs1, rs2, imm
if unsigned(rs1) < unsigned(rs2):
    pc ← pc + imm
else:
    pc ← pc + 4
Branch if Greater or Equal Unsigned BGEU rs1, rs2, imm
if unsigned(rs1) ≥ unsigned(rs2):
    pc ← pc + imm
else:
    pc ← pc + 4
Load Byte LB rd, imm(rs1) rd ← signed(mem[rs1+imm])
Load Half word LH rd, imm(rs1) rd ← signed(mem[rs1+imm:rs1+imm+1])
Load Word LW rd, imm(rs1) rd ← signed(mem[rs1+imm:rs1+imm+3])
Load Byte Unsigned LBU rd, imm(rs1) rd ← unsigned(mem[rs1+imm])
Load Half word Unsigned LHU rd, imm(rs1) rd ← unsigned(mem[rs1+imm:rs1+imm+1])
Store Byte SB rs2, imm(rs1) mem[rs1+imm] ← rs2[7:0]
Store Half word SH rs2, imm(rs1) mem[rs1+imm:rs1+imm+1] ← rs2[15:0]
Store Word SW rs2, imm(rs1) mem[rs1+imm:rs1+imm+3] ← rs2
Add Immediate ADDI rd, rs1, imm rd ← rs1 + imm
Shift Left Logical Immediate SLLI rd, rs1, imm rd ← rs1 sll imm
Set on Less Than Immediate SLTI rd, rs1, imm
if signed(rs1) < signed(imm):
    rd ← 1
else:
    rd ← 0
Set on Less Than Immediate Unsigned SLTIU rd, rs1, imm
if unsigned(rs1) < unsigned(imm):
    rd ← 1
else:
    rd ← 0
Exclusive Or Immediate XORI rd, rs1, imm rd ← rs1 xor imm
Shift Right Logical Immediate SRLI rd, rs1, imm rd ← rs1 srl imm
Shift Right Arithmetic Immediate SRAI rd, rs1, imm rd ← rs1 sra imm
Or Immediate ORI rd, rs1, imm rd ← rs1 or imm
And Immediate ANDI rd, rs1, imm rd ← rs1 and imm
Add ADD rd, rs1, rs2 rd ← rs1 + rs2
Subtract SUB rd, rs1, rs2 rd ← rs1 - rs2
Shift Left Logical SLL rd, rs1, rs2 rd ← rs1 sll rs2
Set on Less Than SLT rd, rs1, rs2
if signed(rs1) < signed(rs2):
    rd ← 1
else:
    rd ← 0
Set on Less Than Unsigned SLTU rd, rs1, rs2
if unsigned(rs1) < unsigned(rs2):
    rd ← 1
else:
    rd ← 0
Exclusive Or XOR rd, rs1, rs2 rd ← rs1 xor rs2
Shift Right Logical SRL rd, rs1, rs2 rd ← rs1 srl rs2[4:0]
Shift Right Arithmetic SRA rd, rs1, rs2 rd ← rs1 sra rs2[4:0]
Or OR rd, rs1, rs2 rd ← rs1 or rs2
And AND rd, rs1, rs2 rd ← rs1 and rs2
Machine Return MRET pc ← mepc

Na tabela acima, operações lógicas e deslocamento tem o seguinte significado:

Operador Efeito
and Bitwise and
or Bitwise or
xor Bitwise exclusive or
sll Logical shift left
srl Logical shift right
sra Arithmetic shift right (com extenção de sinal)

Codificação de Instrução

Uma palavra (word - 4 bytes) de instrução pode ser composta pelos seguintes campos:

O conjunto de instruções RISC-V define seis formatos de instrução:

Formato / Bits 31:25 24:20 19:15 14:12 11:7 6:0
R funct7 rs2 rs1 funct3 rd opcode
I imm[11:5]`/`funct7 imm[4:0] rs1 funct3 rd opcode
S imm[11:5] rs2 rs1 funct3 imm[4:0] opcode
B imm[12,10:5] rs2 rs1 funct3 imm[4:1,11] opcode
U imm[31:25] imm[24:20] imm[19:15] imm[14:12] rd opcode
J imm[20,10:5] imm[4:1,11] imm[19:15] imm[14:12] rd opcode

Valores Immediate (Imediatos)

Valores Immediate são extendidos no sinal (sign-extended) para 32 bits. quando eles não são explicitamente codificados no campo imm, o bit menos significante é 0.

Na especificação, formatos B e J são descritos como variantes dos formatos S e U. Nos formatos B e J, valores immediate representam offsets de instruções ramificações do código relativas. Elas são codificadas para que eles compartilhem o máximo de bits com outros formatos, enqaunto preservam seus bits mais significantes na localização 31 da palavra (word - 4 bytes) de instrução.

A Seguinte tabela mostra o mapeamento entre os bits de uma palavra (word - 4 bytes) de instrução e os bits de um valor imediato:

Formato imm[31:25] imm[24:21] imm[20] imm[19:15] imm[14:12] imm[11] imm[10:5] imm[4:1] imm[0]
I inst[31] inst[31] inst[31] inst[31] inst[31] inst[31] inst[30:25] inst[24:21] inst[20]
S inst[31] inst[31] inst[31] inst[31] inst[31] inst[31] inst[30:25] inst[11:8] inst[7]
B inst[31] inst[31] inst[31] inst[31] inst[31] inst[7] inst[30:25] inst[11:8] 0
U inst[31:25] inst[24:21] inst[20] inst[19:15] inst[14:12] 0 0 0 0
J inst[31] inst[31] inst[31] inst[19:15] inst[14:12] inst[20] inst[30:25] inst[24:21] 0

Opcodes base

No Virgule, nos tempos retido o seguinte opcotes de base da especificação RISC-V. cara opcode corresponde a um formato de instrução específico:

Nome opcode Formato
LOAD 0000011 I
OP-IMM 0010011 I
AUIPC 0010111 U
STORE 0100011 S
OP 0110011 R
LUI 0110111 U
BRANCH 1100011 B
JALR 1100111 I
JAL 1101111 J
SYSTEM 1110011 I

Valores de campos para cada instrução

Quando decodificando uma palavra de instruão, Virgule usa os seguintes campos para identificar a instrução atual. Nesta tabela, a coluna opcode refere-se aos nomes de base do opcode na tabela acima.

Instrução opcode funct3 funct7 rs2
LUI LUI
AUIPCAUIPC
JAL JAL
JALR JALR 000
BEQ BRANCH 000
BNE BRANCH 001
BLT BRANCH 100
BGE BRANCH 101
BLTU BRANCH 110
BGEU BRANCH 111
LB LOAD 000
LH LOAD 001
LW LOAD 010
LBU LOAD 100
LHU LOAD 101
SB STORE 000
SH STORE 001
SW STORE 010
ADDI OP-IMM 000
SLLI OP-IMM 001 0000000
SLTI OP-IMM 010
SLTIUOP-IMM 011
XORI OP-IMM 100
SRLI OP-IMM 101 0000000
SRAI OP-IMM 101 0100000
ORI OP-IMM 110
ANDI OP-IMM 111
ADD OP 000 0000000
SUB OP 000 0100000
SLL OP 001 0000000
SLT OP 010 0000000
SLTU OP 011 0000000
XOR OP 100 0000000
SRL OP 101 0000000
SRA OP 101 0100000
OR OP 110 0000000
AND OP 111 0000000
MRET SYSTEM 000 0011000 00010

Dispositivo de memória e periféricos

Layout de memória

O espaço de endereçamento é organizado como segue.

Endereço (hex) Dispositivos
00000000

00000bff
RAM (3072 bytes)
00000C00

00000fff
Bitmap RAM (1024 bytes)
B0000000
B0000001
Entrada de Texto
C0000000 Saída de Texto
D0000000 Entrada e Saída de propósito geral

Entrada/Saida Texto

O dispositivo de entrada de texto é representado por um campo de texto na interface de usuário do simulador. Ele tem dois registradores de 8-bit:

Endereço (hex) Regra Valor
B0000000 Controle/Estatus Bit 7: Hablita Interrução
Bit 6: Character recebido
B0000001 Data O código ASCII do ultimo caracter inserido.

O registrador de controle/Estatus trabalha da seguinte forma:

O dispositivo de saída de texto é representado por uma área de texto na interface de usuário do simulador. Ele tem somente um registrador somente escrita:

Endereço (hex) Papel Valor
C0000000 Data O código ASCII do caracter para exibir.

Entrada/Saída de propósito geral

O GPIO (Entrada e Saída de propósito geral) de periféricos permite conectar pelo menos 32 dispostivos simples de entrada e saída:

As entradas são organizadas em um grid de 8×4 na parte inferior da seção E/S de propósito geral do simulador. Clique com o botão direito do mouse na célula para trocar o seu tipo. Clique com o botão esquerdo do mouse na célula alterar o estado do botão ou da chave.

Ele tem os seguintes registradores de 32-bits:

Endereço (hex) Papel Valor
D0000000 Direção (dir) A configuração de cada pino (0 para saída, 1 para entrada).
D0000004 Interrupção habilitada (ien) Habilita a interrupção dos eventos de entrada.
D0000008 Eventos subida de borda (rev) Cada bit é definido como 1 se a entrada correspondente alterar o pino de 0 para 1.
D000000C Eventos decida de borda (fev) Cada bit é definido como 1 se a entrada correspondente alterar o pino de 1 para 0.
D0000010 Valor (val) O valor atual de cada entrada ou saída.

Durante o reset, todos os pinos são configurados como entradas e interrupções são desabilitadas.

Eventos de subida de borda ou descida de borda devem ser limpos por software usando instruções de store quando o evento foi processados.

Saída de Bitmap

O simulador fornece uma área de "display" gráfico com 32 linhas com 32 pixels cada. Cada pixel é mapeado para um bate da RAM e sua cor é codificada da seguinte forma:

76543210
Vermelho (Red) Verde (Green) Azul (Blue)

O bitmap na RAM segue uma ordem "raster-scan". Para cada endereço, esta tabela mostra coordenadas (x, y) do pixel correspondente:

+0 +1 +2 +1F
00000C00 (0, 0) (1, 0) (2, 0) (31, 0)
00000C20 (0, 1) (1, 1) (2, 1) (31, 1)
00000C40 (0, 2) (1, 2) (2, 2) (31, 2)
00000FE0 (0, 31) (1, 31) (2, 31) (31, 31)

Criando programas para emulsiV com as ferramentas GNU.

O simulador permite criar e editar programas pela entrada de instruções na columna "Assembly" do visualizador de memória. Outra opçlão é digitar seu programa no editor de texto e gerar um executável para o emulsiV usando as ferramentas GNU.

Instalação

Você pode usar as seguintes intruções para instalar as ferramentas GNU para RISC-V no ambiente Linux Debian ou no Ubuntu. O projeto riscv-gnu-toolchain fornece informações adicionais se você precisa instalar o toolchain em outra distribuição, ou se você precisa recursos que não estão tratados aqui.

Para o processo de "build" serão necessários os seguintes pacotes:

sudo apt install autoconf automake autotools-dev curl libmpc-dev zlib1g-dev \
     libmpfr-dev libgmp-dev gawk build-essential bison flex texinfo gperf \
     libtool patchutils bc zlib1g-dev libexpat-dev git-core

Clone o repositório fonte da ferramenta RISC-V GNU:

git clone --recursive https://github.com/riscv/riscv-gnu-toolchain

Configure e faça o "build" das ferramentas com suporte para o target RV32E. O comando make irá tentar instalar a ferramenta em /opt/riscv. Você pode alterar a opção --prefix se você deseja instalar em outro lugar. Execute o comando sudo make se você não tem permissão para criar o folder destino.

cd riscv-gnu-toolchain
./configure --prefix=/opt/riscv --with-arch=rv32e --with-abi=ilp32
make

Antes de usar as ferramentas, atualize a variável PATH da seguinte forma:

export PATH=$PATH:/opt/riscv/bin

Usando o "assembler"

Este comando compila o arquivo fonte example.s gerando um arquivo de objetos example.o:

riscv32-unknown-elf-gcc -march=rv32e -c -o example.o example.s

Aqui está um módulo típico de inicialização (startup.s) que você pode usar para seu programa. Outros arquivos fontes "assembly" ou "C" devem definir o subprograma main, e opcionalmente sobreescrever o subprograma irq_handler.

    .section vectors, "x"

    .global __reset
__reset:
    j start

__irq:
    j irq_handler

    .text
    .align 4

    .weak irq_handler
irq_handler:
    mret

start:
    la gp, __global_pointer
    la sp, __stack_pointer
    la t0, __bss_start
    la t1, __bss_end
    bgeu t0, t1, memclr_done
memclr:
    sw zero, (t0)
    addi t0, t0, 4
    bltu t0, t1, memclr

memclr_done:
    call main
    j .

Usando o compilador C

Este comando compila o arquivo fonte example.c em um arquivo objeto example.o:

riscv32-unknown-elf-gcc -march=rv32e -ffreestanding -c -o example.o example.c

Se você deseja escrever uma função para tratar uma interrupção em C, você pode sobrescrever o subprograma irq_handler, adicionando o atributo interrupt como a seguir:

__attribute__((interrupt("machine")))
void irq_handler(void) {
    // Insert your code here.
}

Usando o "linker"

Este comando liga startup.o e example.o em um arquivo executável example.elf:

riscv32-unknown-elf-gcc -nostdlib -T emulsiv.ld -o example.elf startup.o example.o

O mapa de memória do do simulador é configurado no "linker script" emulsiv.ld apresentado abaixo:

ENTRY(__reset)

MEM_SIZE    = 4K;
STACK_SIZE  = 512;
BITMAP_SIZE = 1K;

SECTIONS {
    . = 0x0;

    .text : {
        *(vectors)
        *(.text)
        __text_end = .;
    }

    .data   : { *(.data) }
    .rodata : { *(.rodata) }

    __global_pointer = ALIGN(4);

    .bss ALIGN(4) : {
        __bss_start = .;
        *(.bss COMMON)
        __bss_end = ALIGN(4);
    }

    . = MEM_SIZE - STACK_SIZE - BITMAP_SIZE;

    .stack ALIGN(4) : {
        __stack_start = .;
        . += STACK_SIZE;
        __stack_pointer = .;
    }

    .bitmap ALIGN(4) : {
        __bitmap_start = .;
        *(bitmap)
    }

    __bitmap_end = __bitmap_start + BITMAP_SIZE;
}

Convertendo um executável para o formato hex

Este comando converte um arquivo binário do tipo ELF example.elf em um arquivo texto example.hex com o formato Intel Hex:

riscv32-unknown-elf-objcopy -O ihex example.elf example.hex

No simulador, use o botão "Open an hex file from your computer" e escolha arquivo hex para ser carregado.

Licença

emulsiV é um software livre e é distribuido sobre os termos da licença Mozilla Public 2.0.

Este documento foi criado por Guillaume Savaton, ESEO. E é licenciado sobre os termos da licença Creative Commons Attribution-ShareAlike 4.0 International License.