Escrevendo um emulador de CHIP-8
Por conta do meu interesse em jogos antigos e desenvolvimento de jogos, sempre tive vontade de implementar um emulador. Aliás, vontade é palavra forte demais: sempre foi uma ideia que talvez eu nem acreditasse ser capaz de fazer. Os anos foram passando, eu ainda pensava nisso, mas nada. Recentemente eu percebi que bloqueava a ideia mesmo antes de começar a planejar e resolvi parar para encarar o desafio como forma de aprendizado.
Nesses meus sonhos distantes eu desejava implementar um emulador NES só porque é o meu xodó dos consoles. Como isso é muita coisa para uma primeira empreitada no assunto, dei uma pesquisada para encontrar "um processador mais simples de emular" e acabei conhecendo o CHIP-8.
O CHIP-8 não é um hardware, é na verdade uma linguagem de programação interpretada. A especificação da máquina virtual para interpretar a linguagem é bastante similar ao funcionamento de hardwares antigos, por conta disso muitos recomendam escrever um interpretador de CHIP-8 como exercício para aprender a desenvolver um emulador e é isso que vou tentar fazer aqui. Tecnicamente é um interpretador mas vamos seguir chamando de emulador porque nós não somos chatos (não tanto assim, pelo menos agora).
O que é um emulador?
Um emulador é um software capaz de interpretar instruções como se fosse um hardware real. Um emulador de Nintendo NES consegue carregar na memória uma ROM com instruções para NES e fazer com que outro sistema, por exemplo um PC x86_64 com Linux, execute essas instruções como se fosse o hardware real do NES.
O meu objetivo nessa série de textos é escrever um programa em Go que seja capaz de carregar e executar ROMs de CHIP-8.
O CHIP-8
O CHIP-8 foi criado por Joseph Weisbecker em meados dos anos 70. Ele rodava em computadores como o COSMAC VIP e o Telmac 1800, máquinas de 8 bits bem modestas para os padrões de hoje. A ideia era facilitar a escrita de jogos: em vez de programar direto em código de máquina, você escrevia em CHIP-8 e uma máquina virtual interpretava as instruções. Isso deixava os jogos menores e mais fáceis de portar entre as máquinas que tinham um interpretador.
Nos anos 90 a linguagem ganhou uma sobrevida nas calculadoras gráficas da HP, e de lá pra cá virou o "hello world" de quem quer aprender a escrever emuladores. É simples o bastante para caber na cabeça e completo o bastante para ensinar os conceitos que aparecem em consoles de verdade.
Especificação
- Memória: 4 kilobytes de RAM, dos endereços 0x000 a 0xFFF. As ROMs são carregadas a partir de 0x200.
- Display: 64x32 pixels, monocromático.
- Registradores: 16 registradores de 8 bits, de V0 a VF. O VF tem um papel especial e é usado como flag por várias instruções (carry, borrow e colisão de sprites).
- Registrador I: 16 bits, aponta para endereços de memória. É usado principalmente para desenhar sprites.
- Program Counter (PC): 16 bits, aponta para a instrução atual. Como cada instrução ocupa 2 bytes, o PC avança de 2 em 2.
- Stack: guarda os endereços de retorno das sub-rotinas, com espaço para 16 níveis. Trabalha junto com um stack pointer.
- Timers: um delay timer e um sound timer, ambos de 8 bits, decrementados a 60 Hz.
- Teclado: hexadecimal, com 16 teclas de 0 a F.
- Cerca de 35 opcodes, cada um com 16 bits.
Uma curiosidade sobre a memória: os primeiros 512 bytes (de 0x000 a 0x1FF) eram reservados para o próprio interpretador do CHIP-8, que ficava carregado ali junto com o programa. É por isso que as ROMs começam em 0x200. Hoje não precisamos daquele espaço para o interpretador, então aproveitamos ele para guardar a fonte com os dígitos hexadecimais.
Próximos passos
O próximo passo é sair da teoria e escrever código. No próximo post a gente monta o esqueleto do projeto em Go e traduz essa especificação para uma struct: a memória, os registradores, o índice, o program counter, a stack e os timers, tudo o que descrevemos aqui virando campo da nossa CPU. Também vamos carregar aquela fonte hexadecimal na memória, o primeiro pedaço de estado que a máquina precisa ter antes de rodar qualquer ROM.
Comentários