3

Construindo o seu próprio emulador, parte 1

 2 years ago
source link: https://dev.to/mauricioabreu/construindo-o-seu-proprio-emulador-parte-1-3a1o
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

O que é um emulador?

Um emulador emula hardware em software. É uma técnica que habilita um computador imitar as características de outro hardware, como por exemplo, um video game. Um famoso emulador é o Project64, usado para emular o video game Nintendo 64.

Além de emular, um emulador pode ir adiante, providenciando uma maior performance, maior qualidade de vídeo e melhor gerenciamento de recursos como CPU e memória.

Por que aprender sobre emuladores?

Boa pergunta! Entender o básico de emuladores vai te proporcionar uma boa ideia de como computadores funcionam.

Emuladores são compostos de vários componentes de um computador, como uma memória, uma CPU, um teclado e um display. Cada um desses componentes tem suas características. O desenvolvimento de um emulador vai te ajudar a entender como um programa é carregado na memória (ROM, read-only memory), ou como as instruções do programa são interpretadas e executadas e como as informações são mostradas na tela.

Alguns requisitos são necessários para entender e construir um, como:

  • Conhecer alguma linguagem de programação;
  • Operações lógicas;
  • Deslocamento de bits (bit shifting).

Se você conhece uma linguagem de programação mas não entende sobre operações lógicas e operações de bit, não se preocupe, eu vou colocar referências de estudo e explicar as partes do código que falam sobre isso em detalhes.

O hello world! dos emuladores é o chip-8, e é ele que vamos construir.

Um pouco de base

Antes de começarmos a falar sobre o emulador que vamos construir, vamos relembrar ou aprender um pouco sobre dois assuntos que precisamos compreender para seguir este guia.

Binário e hexadecimal

Nosso sistema número mais comum é o sistema decimal. Isso quer dizer que temos do 0 ao 9 para formarmos outros números, como o 2, o 321, o 9825, etc. Depois do 9, para adicionarmos um número passamos o 9 para 0 e colocamos 1 à esquerda e teremos o 10. Computadores usam outros sistemas de números: binário e hexadecimal.

O sistema binário é constituído de 2 números, o 0 e o 1. Zero é 0, um é 1 e o 2 troca o 1 pra 0 e adiciona um 1 à esquerda. Contando em binário: 0, 1, 10, 11, 100, 101, 110, 111, 1000 (0, 1, 2, 3, 4, 5, 6, 7, 8).

O sistema hexadecimal conta com 16 símbolos. Isso provê uma maior compactação na representação de números maiores. Por exemplo o 15 é representado pela letra F. Quanto maior o número, mais isso fica perceptível.

Todas as instruções do chip-8 são analisadas de forma hexadecimal. A instrução de limpar a tela começa com 0x0 e a operação de desenhar é 0xD.

Lógica binária

Lógica binária opera em bits. Usamos para manipular valores e fazer comparações.

Algumas dessas operações você já pode conhecer, mas uma em especial é importante conhecer, o XOR. O XOR devolve 1 bit sempre que a quantidade de números 1 for ímpar:

 |  V1 |  V2 |  R  |
 +-----+-----+-----+
 |  0  |  0  |  0  |
 +-----+-----+-----+
 |  1  |  0  |  1  |
 +-----+-----+-----+
 |  0  |  1  |  1  |
 +-----+-----+-----+
 |  1  |  1  |  0  |
 +-----+-----+-----+
Enter fullscreen modeExit fullscreen mode

Você pode testar isso com alguma linguagem com REPL ou usando bash:

echo $((0 ^ 0))
0
echo $((0 ^ 1))
1
echo $((1 ^ 1))
0
Enter fullscreen modeExit fullscreen mode

Deslocamento de bits

Uma das razões do deslocamento de bits existir é a possibilidade de codificar informações importantes usando menos dados. O receptor da mensagem pode decodificar a mensagem, extraindo valores com significado.

As instruções do chip-8, por exemplo, são codificadas de 2 em 2 bytes, e cada byte pode ser dividido em 2 nibbles usando deslocamento de bits. Veremos isso em mais detalhes no próximo post, quando vamos aprender a decodificar as instruções da ROM.

Geralmente a primeira instrução de uma ROM é limpar a tela, a 00E0. Em binário isso seria 00000000 (00) e 11100000 (E0).

A tela é limpa quando o OpCode é 0 e o quarto nibble é 0. Vamos testar usando o REPL do Python:

>>> (0b00000000 >> 4) & 0xF // OpCode
0
>>> 0b11100000 & 0xF // quarto nibble chamado de N
0
Enter fullscreen modeExit fullscreen mode

A instrução de jump (0x1NNN) faz o código pular até a instrução de memória NNN.

0b11010 | 0b100101
1A      | 25

JUMP | 0xA25
0x1  | A25
Enter fullscreen modeExit fullscreen mode

0x1 é a operação de pular e A25 forma o NNN, o quarto, terceiro e segundo nibble dos dois bytes da instrução.

Veremos esse assunto em detalhes na parte 2.

O que é o chip-8?

O chip-8 é uma linguagem interpretada e, também, uma máquina virtual criada por Joe Weisbecker em 1977 para rodar no COSMAC VIP

A ideia do Joe era rodar pequenos programas e jogos com a ajuda de um teclado de hexadecimal. Em vez de usar linguagem de máquina, o teclado hexadecimal era usado para digitar instruções que seriam interpretadas.

Interpretadas? O correto não seria emuladas? Não. Nesse guia não vamos construir um emulador exatamente, mas sim um interpretador de instruções chip-8. Nosso interpretador vai ler instruções de uma ROM e executá-las uma a uma, em um loop de ler, decodificar e executar as instruções carregadas.

Mas qual a diferença entre interpretador e emulador nesse caso? O chip-8 é um programa que rodava em um computador. Um emulador imita hardware. Simular o chip-8 significa que vamos escrever uma máquina virtual que interpreta comandos via uma linguagem hexadecimal, porém, trazendo vários conceitos vistos em emulação e arquitetura de computadores como PC (program counter), stack, timers, RAM, ROM, etc.

O chip-8 é formado por diversos componentes. Abaixo vamos falar de cada um dos componentes de maneira breve.

Memória

O espaço de memória deve ser de 4kB (4096 bytes). Toda essa memória é volátil (RAM) e pode ser modificável.

Uma ROM deve começar começar a ser carregada a partir do endereço 0x200 (512 em decimal). Os endereços 0x000 até o 0x1FF são reservados para o chip-8, porém vamos construir ele usando nosso computador, então não precisamos nos preocupar com isso.

Registradores

Existem dois registradores: dados e de endereço.
Eles são usados para gerenciar dados no chip-8. Há várias operações como adição, subtração, leitura de valores, etc. Essas operações se tornam possíveis quando você tem registradores.

O registrador de dados é um array com 16 posições de valor inteiro de 8 bits sem sinal (u8). O endereço 0xF é normalmente usado para configurar uma flag (0 ou 1), que pode ser usado para indicar se houve colisão ao desenhar no display.

O registrador de endereço não é um array, mas sim uma espécie de ponteiro. Ele é chamado de index e é usado para ler e escrever na memória. O registrador de endereços, chamado de I, também é usado para desenhar as fontes no display, que serão configuradas através de uma das instruções contidas na ROM.

Display

Com as dimensões de 64 pixels de largura e 32 pixels (64x32) de altura, o display é usado para renderizar na tela toda atualização identificada pela instrução de draw (DXYN) que veremos adiante.

No chip-8, os pixels são valores booleanos, 0 ou 1, on ou off, 0x0 ou 0x1. Os displays eram monocromáticos (preto e branco), porém, quando chegar a hora você vai poder usar diferentes cores para os pixels. O display começa com todos os pixels off.

Em desenvolvimento você pode, em vez de escrever na tela usando alguma engine de gráficos, escrever no STDOUT para conferir se o programa está desenhado da maneira correta.

Teclado

O teclado do COSMAC VIP tem 16 teclas, por isso é chamado de hex keypad. Não vamos construir o teclado, mas precisamos saber que ele existe e que algumas instruções do chip-8 esperam por uma tecla a ser pressionada.

Ao construir seu chip-8 você não precisa seguir esse layout. O layout que usei é como mostrado abaixo:

╔═══╦═══╦═══╦═══╗
║ 1 ║ 2 ║ 3 ║ 4 ║
╠═══╬═══╬═══╬═══╣
║ Q ║ W ║ E ║ R ║
╠═══╬═══╬═══╬═══╣
║ A ║ S ║ D ║ F ║
╠═══╬═══╬═══╬═══╣
║ Z ║ X ║ C ║ V ║
╚═══╩═══╩═══╩═══╝
Enter fullscreen modeExit fullscreen mode

Se o seu teclado não é no layout QWERTY, tudo bem, você pode mapear outras teclas ou até deixar isso configurável.

Stack

A stack é uma área de memória relacionada a sub-rotinas que podem ser chamadas durante a execução da ROM.
Como o próprio nome já diz, isso é uma stack. Se a sua linguagem de programação suporta o uso de stacks, use-a para maior legibilidade.

Os valores guardados nessa stack são de 16 bits.

Em Rust, os vectors possuem as funções push e pop:

let mut stack: Vec<u16> = Vec::new();
stack.push(0xF);
stack.pop();
Enter fullscreen modeExit fullscreen mode

Existe uma limitação no chip-8 de 12 a 16 endereços de memória, porém, não precisamos nos preocupar com isso, deixando o tamanho da stack ilimitada.

Fontes

O chip-8 vem com fontes pré-determinadas. Isso significa que existe na memória do chip-8 sprites que são basicamente números e letras no intervalo hexadecimal: 0 a F. Cada uma dessas representações tem 4 pixels de largura e 5 pixels de altura.

Guarde essas fontes em uma área de memória antes do 0x200 que falamos acima. Por algum motivo ficou popular gravar a partir do 0x50. É possível definir e carregar as fontes usando um código similar ao debaixo:

static FONTS: [u8; 80] = [
    0xF0, 0x90, 0x90, 0x90, 0xF0, // 0
    0x20, 0x60, 0x20, 0x20, 0x70, // 1
    0xF0, 0x10, 0xF0, 0x80, 0xF0, // 2
    0xF0, 0x10, 0xF0, 0x10, 0xF0, // 3
    0x90, 0x90, 0xF0, 0x10, 0x10, // 4
    0xF0, 0x80, 0xF0, 0x10, 0xF0, // 5
    0xF0, 0x80, 0xF0, 0x90, 0xF0, // 6
    0xF0, 0x10, 0x20, 0x40, 0x40, // 7
    0xF0, 0x90, 0xF0, 0x90, 0xF0, // 8
    0xF0, 0x90, 0xF0, 0x10, 0xF0, // 9
    0xF0, 0x90, 0xF0, 0x90, 0x90, // A
    0xE0, 0x90, 0xE0, 0x90, 0xE0, // B
    0xF0, 0x80, 0x80, 0x80, 0xF0, // C
    0xE0, 0x90, 0x90, 0x90, 0xE0, // D
    0xF0, 0x80, 0xF0, 0x80, 0xF0, // E
    0xF0, 0x80, 0xF0, 0x80, 0x80, // F
];

for (n, font) in FONTS.iter().enumerate() {
    machine.memory[0x50 + n] = *font;
}
Enter fullscreen modeExit fullscreen mode

Existe uma instrução (FX29) que guarda o endereço da fonte a ser desenhada. Então, a instrução de desenhar vai usar esse valor de memória para escrever a fonte na tela.

Timers

O chip-8 conta com 2 timers (temporizadores): delay timer e sound timer.

Ambos timers são decrementados numa taxa de 60khz até chegar a zero. Ao chegar em zero, cada um terá seu evento interrompido e a contagem recomeça.

O delay timer é usado para sincronizar eventos.
O sound timer vai tocar um som (beep) enquanto o valor for maior que zero.

Os dois timers podem ser inicializados como um inteiro de 8 bits sem sinal (u8).

if self.delay_timer > 0 {
    self.delay_timer -= 1;
}

if self.sound_timer > 0 {
    self.sound_timer -= 1;
}
Enter fullscreen modeExit fullscreen mode

No próximo post vamos abordar, em detalhes, a codificação do chip-8 usando a linguagem Rust, entendendo como decodificar as instruções e executar as operações.


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK