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. Existem emuladores famosos como o Project64 e o ZSNES, usados para emular os video games Nintendo 64 e Super Nintendo respectivamente.
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 |
+-----+-----+-----+
Você pode testar isso com alguma linguagem com REPL ou usando bash
:
echo $((0 ^ 0))
0
echo $((0 ^ 1))
1
echo $((1 ^ 1))
0
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
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
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 ║
╚═══╩═══╩═══╩═══╝
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();
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;
}
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;
}
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.
Top comments (0)