Partes do
primeiro ,
segundo ,
terceiro .
O resto da máquina
O código que escrevemos para emular o processador 8080 é bastante geral e pode ser facilmente adaptado para rodar em qualquer máquina com o compilador C. Mas, para jogar o jogo em si, precisamos fazer mais. Teremos que emular o equipamento de toda a máquina de fliperama e escrever um código que cole os recursos específicos do nosso ambiente de computação no emulador.
(Você pode estar interessado em olhar para o
diagrama de circuito da máquina.)
Horários
O jogo roda nos 2 MHz 8080. Seu computador é muito mais rápido. Para levar isso em conta, teremos que criar algum tipo de mecanismo.
Interrupções
As interrupções são projetadas para que o processador possa processar tarefas com tempos de execução precisos, como E / S. O processador pode executar o programa e, quando o pino de interrupção é acionado, para de executar o programa atual e faz outra coisa.
Precisamos simular a maneira como uma máquina de fliperama gera interrupções.
Gráficos
Invasores do espaço desenham gráficos para sua memória no intervalo de endereços 0x2400. Um controlador de vídeo de hardware real lê a RAM e controla um monitor CRT. Nosso programa precisará emular esse comportamento renderizando uma imagem do jogo em uma janela.
Botões
O jogo possui botões físicos que o programa lê usando o comando IN do processador 8080. Nosso emulador precisará vincular a entrada do teclado a esses comandos IN.
ROM e RAM
Devo admitir: “cortamos os cantos” criando um buffer de memória de 16 kilobytes, que inclui os 16 KB inferiores da alocação de memória do processador. De fato, os primeiros 2 KB de alocação de memória são uma memória real somente leitura (ROM). Nós precisaremos colocar operações de gravação na memória em uma função para que não seja possível gravar na ROM.
Som
Até agora, não dissemos nada sobre som. O Space Invaders possui um bonito esquema de som analógico que reproduz um dos 8 sons controlados pelo comando OUT, que é transmitido a uma das portas. Teremos que converter esses comandos OUT para reproduzir amostras de som em nossa plataforma.
Pode parecer muito trabalho, mas não é tão ruim, e podemos nos mover gradualmente. A primeira coisa que queremos fazer é ver a tela, para a qual precisamos de interrupções, gráficos e parte do processamento dos comandos IN e OUT.
Monitores e atualização
O básico
Você provavelmente está familiarizado com os componentes de um sistema de exibição de vídeo. Em algum lugar do sistema, existe algum tipo de RAM, que contém uma imagem para exibição na tela. No caso de dispositivos analógicos, existe um equipamento que lê essa RAM e converte os bytes em tensão analógica transmitida ao monitor.
Uma compreensão mais profunda do sistema nos ajudará quando se trata de analisar o objetivo da alocação de memória e da funcionalidade do código.
Os monitores analógicos têm requisitos para taxas e tempos de atualização. A qualquer momento, a tela possui um pixel específico atualizado. A imagem transmitida para a tela é preenchida ponto a ponto, começando no canto superior esquerdo e no canto superior direito, depois o primeiro ponto da segunda linha, o último ponto da segunda linha, etc. Depois que a última linha é desenhada na tela, o controlador de vídeo pode gerar uma interrupção vertical em branco (também conhecida como VBI ou VBL).
Para garantir uma animação suave, a imagem na RAM processada pelo controlador de vídeo não pode ser alterada. Se a atualização da RAM ocorreu no meio do quadro, o visualizador verá partes de duas imagens. Isso resulta em um efeito de “ruptura” quando um quadro diferente do quadro na parte inferior é exibido na parte superior da tela. Se você já viu uma quebra de linha, sabe como é.
Para evitar falhas, o software deve fazer algo para evitar a transferência do local da atualização da tela. E há apenas uma maneira de fazer isso.
O VBL é gerado após o final da última linha e, geralmente, há um certo período de tempo antes de redesenhar a primeira linha. (Este é o tempo em branco vertical e pode demorar cerca de 1 milissegundo.)
Quando o VBL é recebido, o programa começa a renderizar a tela de cima.
Cada linha é desenhada antes do processo reverso da digitalização de quadros.
A CPU está sempre à frente do retorno a quente e, portanto, evita quebras de linha.
Space Invaders Video System
Uma
página muito informativa nos diz que os Space Invaders têm duas interrupções de vídeo. Uma é para o final do quadro, mas também gera uma interrupção no meio da tela. A página descreve o sistema de atualização de tela - o jogo desenha gráficos na metade superior da tela quando recebe uma interrupção no meio da tela e desenha gráficos na parte inferior da tela quando recebe uma interrupção no final do quadro. Essa é uma maneira bastante inteligente de eliminar quebras de linha e um bom exemplo do que pode ser alcançado quando você desenvolve hardware e software ao mesmo tempo.
Devemos forçar a emulação de nossa máquina a gerar essas interrupções. Se os gerarmos com uma frequência de 60 Hz, assim como a máquina Space Invaders, o jogo será desenhado com a frequência correta.
Na próxima seção, falaremos sobre a mecânica das interrupções e pensaremos em como imitá-las.
Botões e portas
O 8080 implementa E / S usando as instruções IN e OUT. Possui 8 portas IN e OUT separadas - a porta é determinada pelo byte de dados do comando. Por exemplo,
IN 3
colocará o valor da porta 3 no registro A e
OUT 2
enviará A para a porta 2.
Peguei informações sobre o objetivo de cada porta no site de
Arqueologia do
Computador . Se essas informações não estivessem disponíveis, teríamos que obtê-las estudando o diagrama de circuitos, bem como lendo e executando o código passo a passo.
:
1
0 (0, )
1 Start
2 Start
3 ?
4
5
6
7 ?
2
0,1 DIP- (0:3,1:4,2:5,3:6)
2 ""
3 DIP- , 1:1000,0:1500
4
5
6
7 DIP-, 1:,0:
3
2 ( 0,1,2)
3
4
5
6 "" ? , ,
(0=a,1=b,2=c ..)
( 3,5,6 1=$01 2=$00
, (attract mode))
Há três maneiras de implementar E / S em nossa pilha de software (que consiste em um emulador 8080, código de máquina e código de plataforma).
- Incorporar conhecimento de máquina em nosso emulador 8080
- Incorporar conhecimento do emulador 8080 no código da máquina
- Invente uma interface formal entre as três partes do código para permitir a troca de informações por meio da API
Eu descartei a primeira opção - é bastante óbvio que o emulador está na parte inferior desta cadeia de chamadas e deve permanecer separado. (Imagine que você precise reutilizar o emulador para outro jogo e entenderá o que quero dizer.) No caso geral, transferir estruturas de dados de alto nível para níveis mais baixos é uma solução arquitetônica ruim.
Eu escolhi a opção 2. Deixe-me mostrar o código primeiro:
while (!done) { uint8_t opcode = state->memory[state->pc]; if (*opcode == 0xdb)
Esse código reimplementa o processamento de opcodes para IN e OUT na mesma camada, que chama o emulador para o restante dos comandos. Na minha opinião, isso torna o código mais limpo. Isso é semelhante a uma substituição ou subclasse para os dois comandos, que se refere a uma camada de autômato.
A desvantagem é que transferimos a emulação de códigos de operação em dois lugares. Não vou te culpar por escolher a terceira opção. Na segunda opção, menos código é necessário, mas a opção 3 é mais "limpa", mas o preço é um aumento na complexidade. Esta é uma questão de escolha de estilo.
Registo de turno
A máquina Space Invaders possui uma solução de hardware interessante que implementa um comando de troca de bits. O 8080 possui comandos para um turno de 1 bit, mas dezenas de comandos do 8080 serão necessários para implementar um turno de vários bits / vários bytes.O hardware especial permite que o jogo realize essas operações com apenas algumas instruções. Com sua ajuda, cada quadro é desenhado no campo de jogo, ou seja, é usado muitas vezes por quadro.
Acho que não posso explicar melhor do que a excelente
análise da Arqueologia
de Computadores:
; 16- :
; f 0
; xxxxxxxxyyyyyyyy
;
; 4 x y, x, :
; $0000,
; write $aa -> $aa00,
; write $ff -> $ffaa,
; write $12 -> $12ff, ..
;
; 2 ( 0,1,2) 8- , :
; offset 0:
; rrrrrrrr result=xxxxxxxx
; xxxxxxxxyyyyyyyy
;
; offset 2:
; rrrrrrrr result=xxxxxxyy
; xxxxxxxxyyyyyyyy
;
; offset 7:
; rrrrrrrr result=xyyyyyyy
; xxxxxxxxyyyyyyyy
;
; 3 .
Para o comando OUT, gravar na porta 2 define a quantidade de turnos e gravar na porta 4 define os dados nos registradores de turnos. A leitura com IN 3 retorna dados alterados pela quantidade de turno. Na minha máquina, isso é implementado assim:
-(uint8_t) MachineIN(uint8_t port) { uint8_t a; switch(port) { case 3: { uint16_t v = (shift1<<8) | shift0; a = ((v >> (8-shift_offset)) & 0xff); } break; } return a; } -(void) MachineOUT(uint8_t port, uint8_t value) { switch(port) { case 2: shift_offset = value & 0x7; break; case 4: shift0 = shift1; shift1 = value; break; } }
Teclado
Para obter a resposta da máquina, precisamos vincular a entrada do teclado a ela. A maioria das plataformas tem uma maneira de receber pressionamentos de tecla e liberar eventos. O código da plataforma para os botões será semelhante ao seguinte:
if(PeekMessage(&msg,NULL,0,0,PM_REMOVE)) { if (msg.message==WM_KEYDOWN ) { if ( msg.wParam == VK_LEFT ) MachineKeyDown(LEFT); } else if (msg.message==WM_KEYUP ) { if ( msg.wParam == VK_LEFT ) MachineKeyUp(LEFT); } }
O código da máquina que cola o código da plataforma no código do emulador será mais ou menos assim:
MachineKeyDown(char key) { switch(key) { case LEFT: port[1] |= 0x20;
Se desejar, você pode combinar o código da máquina e a plataforma conforme desejar - esta é a escolha da implementação. Não farei isso porque vou portar a máquina para várias plataformas diferentes.
Interrupções
Tendo estudado o manual, percebi que o 8080 lida com interrupções da seguinte maneira:
- A fonte de interrupção (externa à CPU) define o pino de interrupção da CPU.
- Quando a CPU confirma que a interrupção foi recebida, a fonte da interrupção pode enviar qualquer código operacional para o barramento e a CPU o vê. (Na maioria das vezes, eles usam o comando RST.)
- A CPU executa este comando. Se for RST, este é um análogo do comando CALL para um endereço fixo na parte inferior da memória. Empurra o PC atual para a pilha.
- O código no endereço de memória inferior processa o que a interrupção deseja dizer ao programa. Após o término do processamento, o RST termina com uma chamada para o RET.
O equipamento de vídeo do jogo gera duas interrupções que devemos imitar programaticamente: o final do quadro e o meio do quadro. Ambos são realizados a 60 Hz (60 vezes por segundo). 1/60 de segundo é 16,6667 milissegundos.
Para simplificar o trabalho com interrupções, adicionarei uma função ao emulador 8080:
void GenerateInterrupt(State8080* state, int interrupt_num) {
O código da plataforma deve implementar um timer que podemos chamar (por enquanto, eu apenas chamo de tempo ()). O código da máquina o utilizará para passar uma interrupção para o emulador 8080. No código da máquina, quando o cronômetro expirar, chamarei GenerateInterrupt:
while (!done) { Emulate8080Op(state); if ( time() - lastInterrupt > 1.0/60.0)
Existem alguns detalhes de como o 8080 realmente lida com interrupções, que não iremos imitar. Acredito que esse processamento será suficiente para nossos propósitos.