Création d'une machine d'arcade d'émulation. Partie 4

image

Parties du premier , deuxième , troisième .

Le reste de la machine


Le code que nous avons écrit pour émuler le processeur 8080 est assez général et peut être facilement adapté pour fonctionner sur n'importe quelle machine avec le compilateur C. Mais pour jouer au jeu lui-même, nous devons faire plus. Nous devrons émuler l'équipement de toute la machine d'arcade et écrire du code qui colle les caractéristiques spécifiques de notre environnement informatique à l'émulateur.

(Vous pouvez être intéressé par le schéma de circuit de la machine.)

Timings


Le jeu fonctionne sur le 2 MHz 8080. Votre ordinateur est beaucoup plus rapide. Pour en tenir compte, nous devrons trouver une sorte de mécanisme.

Interruptions


Les interruptions sont conçues pour que le processeur puisse traiter des tâches avec des temps d'exécution précis, comme les E / S. Le processeur peut exécuter le programme et lorsque la broche d'interruption est déclenchée, il arrête d'exécuter le programme en cours et fait autre chose.

Nous devons simuler la façon dont une machine d'arcade génère des interruptions.

Graphisme


Space Invaders dessine des graphiques dans sa mémoire dans la plage d'adresses 0x2400. Un véritable contrôleur vidéo matériel lirait la RAM et contrôlerait un écran CRT. Notre programme devra émuler ce comportement en affichant une image du jeu dans une fenêtre.

Boutons


Le jeu possède des boutons physiques que le programme lit à l'aide de la commande IN du processeur 8080. Notre émulateur devra lier l'entrée clavier à ces commandes IN.

ROM et RAM


Je dois admettre: nous «coupons le coin» en créant une mémoire tampon de 16 kilo-octets, qui inclut les 16 Ko inférieurs de l'allocation de mémoire du processeur. En fait, les 2 premiers Ko d'allocation de mémoire sont une véritable mémoire morte (ROM). Nous devrons mettre des opérations d'écriture en mémoire dans une fonction afin qu'il ne soit pas possible d'écrire sur la ROM.

Son


Jusqu'à présent, nous n'avons rien dit sur le son. Space Invaders a un joli modèle de son analogique qui reproduit l'un des 8 sons contrôlés par la commande OUT, qui est transmis à l'un des ports. Nous devrons convertir ces commandes OUT afin de lire des échantillons sonores sur notre plateforme.

Cela peut sembler beaucoup de travail, mais ce n'est pas si mal, et nous pouvons évoluer progressivement. La première chose que nous voulons faire est de voir l'écran, pour lequel nous avons besoin d'interruptions, de graphiques et d'une partie du traitement des commandes IN et OUT.

Affiche et met à jour


Les bases


Vous connaissez probablement les composants d'un système d'affichage vidéo. Quelque part dans le système, il existe une sorte de RAM, qui contient une image à afficher à l'écran. Dans le cas des appareils analogiques, il existe un équipement qui lit cette RAM et convertit les octets en tension analogique transmise au moniteur.

Une compréhension plus approfondie du système nous aidera à analyser le but de l'allocation de mémoire et de la fonctionnalité de code.

Les écrans analogiques ont des exigences en termes de taux de rafraîchissement et de temporisation. À tout moment, l'affichage a un pixel spécifique mis à jour. L'image transmise à l'écran est remplie point par point, en partant du coin supérieur gauche et en haut à droite, puis du premier point de la deuxième ligne, du dernier point de la deuxième ligne, etc. Une fois la dernière ligne tracée à l'écran, le contrôleur vidéo peut générer une interruption verticale vierge (également connue sous le nom de VBI ou VBL).

Pour assurer une animation fluide, l'image en RAM traitée par le contrôleur vidéo ne peut pas être modifiée. Si la mise à jour de la RAM s'est produite au milieu du cadre, le spectateur verra des parties de deux images. Il en résulte un effet de «déchirure» lorsqu'un cadre différent du cadre en bas est affiché en haut de l'écran. Si vous avez déjà vu un saut de ligne, vous savez à quoi il ressemble.

Pour éviter les lacunes, le logiciel doit faire quelque chose pour éviter de transférer l'emplacement de la mise à jour de l'écran. Et il n'y a qu'une seule façon de procéder.

VBL est généré après la fin de la dernière ligne, et il y a généralement un certain temps avant de redessiner la première ligne. (Il s'agit du temps de blanc vertical et il peut être d'environ 1 milliseconde.)

Lorsque VBL est reçu, le programme commence à rendre l'écran par le haut.

Chaque ligne est tracée avant le processus inverse de balayage d'image.

Le CPU est toujours en avance sur le retour à chaud, et évite donc les sauts de ligne.

image

Système vidéo Space Invaders


Une page très informative nous apprend que les Space Invaders ont deux interruptions vidéo. L'une concerne la fin du cadre, mais elle génère également une interruption au milieu de l'écran. La page décrit le système de mise à jour de l'écran - le jeu dessine des graphiques dans la moitié supérieure de l'écran lorsqu'il reçoit une interruption au milieu de l'écran et dessine des graphiques dans la partie inférieure de l'écran lorsqu'il reçoit une interruption à la fin de la trame. C'est un moyen assez intelligent d'éliminer les sauts de ligne et un bon exemple de ce qui peut être réalisé lorsque vous développez du matériel et des logiciels en même temps.

Nous devons forcer l'émulation de notre machine pour générer de telles interruptions. Si nous les générons avec une fréquence de 60 Hz, ainsi que la machine Space Invaders, alors le jeu sera dessiné avec la bonne fréquence.

Dans la section suivante, nous parlerons de la mécanique des interruptions et réfléchirons à la façon de les émuler.

Boutons et ports


Le 8080 implémente les E / S à l'aide des instructions IN et OUT. Il dispose de 8 ports IN et OUT distincts - le port est déterminé par l'octet de données de la commande. Par exemple, IN 3 mettra la valeur du port 3 dans le registre A et OUT 2 enverra A au port 2.

J'ai pris des informations sur l'objectif de chaque port sur le site Web de Computer Archaeology . Si ces informations n'étaient pas disponibles, il faudrait les obtenir en étudiant le schéma électrique, ainsi qu'en lisant et en exécutant le code étape par étape.

:
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))


Il existe trois façons d'implémenter les E / S dans notre pile logicielle (qui comprend un émulateur 8080, un code machine et un code de plate-forme).

  1. Intégrez les connaissances de la machine dans notre émulateur 8080
  2. Intégrer la connaissance de l'émulateur 8080 dans le code machine
  3. Inventer une interface formelle entre les trois parties du code pour permettre l'échange d'informations via l'API

J'ai exclu la première option - il est assez évident que l'émulateur est tout en bas de cette chaîne d'appel et doit rester séparé. (Imaginez que vous devez réutiliser l'émulateur pour un autre jeu, et vous comprendrez ce que je veux dire.) Dans le cas général, le transfert de structures de données de haut niveau à des niveaux inférieurs est une mauvaise solution architecturale.

J'ai choisi l'option 2. Permettez-moi de montrer le code en premier:

  while (!done) { uint8_t opcode = state->memory[state->pc]; if (*opcode == 0xdb) //machine specific handling for IN { uint8_t port = opcode[1]; state->a = MachineIN(state, port); state->pc++; } else if (*opcode == 0xd3) //OUT { uint8_t port = opcode[1]; MachineOUT(state, port); state->pc++; } else Emulate8080Op(state); } 

Ce code réimplémente le traitement des opcodes pour IN et OUT dans la même couche, qui appelle l'émulateur pour le reste des commandes. À mon avis, cela rend le code plus propre. Ceci est similaire à un remplacement ou à une sous-classe pour les deux commandes, qui fait référence à une couche automate.

L'inconvénient est que nous transférons l'émulation des opcodes à deux endroits. Je ne vous blâmerai pas d'avoir choisi la troisième option. Dans la deuxième option, moins de code est requis, mais l'option 3 est plus «propre», mais le prix est une augmentation de complexité. C'est une question de choix de style.

Registre à décalage


La machine Space Invaders dispose d'une solution matérielle intéressante qui implémente une commande de décalage de bits. Le 8080 a des commandes pour un décalage de 1 bit, mais des dizaines de commandes 8080 seront nécessaires pour implémenter un décalage multi-bits / multi-octets. Un matériel spécial permet au jeu d'effectuer ces opérations en quelques instructions seulement. Avec son aide, chaque image est dessinée sur le terrain de jeu, c'est-à-dire qu'elle est utilisée plusieurs fois par image.

Je ne pense pas pouvoir mieux l'expliquer que l'excellente analyse de l'archéologie informatique:

; 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 .


Pour la commande OUT, l'écriture sur le port 2 définit la quantité de décalage et l'écriture sur le port 4 définit les données dans les registres à décalage. La lecture avec IN 3 renvoie des données décalées de la quantité de décalage. Dans ma machine, cela est implémenté comme ceci:

  -(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; } } 

Clavier


Pour obtenir la réponse de la machine, nous devons lui associer une entrée clavier. La plupart des plateformes ont un moyen de recevoir les événements de frappe et de libération. Le code de plate-forme pour les boutons ressemblera à ceci:

  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); } } 

Le code machine collant le code de la plateforme au code de l'émulateur ressemblera à ceci:

  MachineKeyDown(char key) { switch(key) { case LEFT: port[1] |= 0x20; //Set bit 5 of port 1 break; case RIGHT: port[1] |= 0x40; //Set bit 6 of port 1 break; /*....*/ } } PlatformKeyUp(char key) { switch(key) { case LEFT: port[1] &= 0xDF //Clear bit 5 of port 1 break; case RIGHT: port[1] &= 0xBF //Clear bit 6 of port 1 break; /*....*/ } } 

Si vous le souhaitez, vous pouvez combiner le code de la machine et de la plateforme à votre guise - c'est le choix de l'implémentation. Je ne le ferai pas car je vais porter la machine sur plusieurs plates-formes différentes.

Interruptions


Après avoir étudié le manuel, j'ai réalisé que le 8080 gère les interruptions comme suit:

  1. La source d'interruption (externe au CPU) définit la broche d'interruption du CPU.
  2. Lorsque le CPU confirme que l'interruption est reçue, la source de l'interruption peut envoyer n'importe quel opcode au bus et le CPU le voit. (Le plus souvent, ils utilisent la commande RST.)
  3. Le CPU exécute cette commande. S'il s'agit de RST, il s'agit alors d'un analogue de la commande CALL pour une adresse fixe au bas de la mémoire. Il pousse le PC actuel sur la pile.
  4. Le code de l'adresse mémoire inférieure traite ce que l'interruption veut dire au programme. Une fois le traitement terminé, RST se termine par un appel à RET.

L'équipement vidéo du jeu génère deux interruptions que nous devons émuler par programmation: la fin de l'image et le milieu de l'image. Les deux sont exécutés à 60 Hz (60 fois par seconde). 1/60 de seconde est de 16,6667 millisecondes.

Pour simplifier le travail avec les interruptions, j'ajouterai une fonction à l'émulateur 8080:

  void GenerateInterrupt(State8080* state, int interrupt_num) { //perform "PUSH PC" Push(state, (state->pc & 0xFF00) >> 8, (state->pc & 0xff)); //Set the PC to the low memory vector. //This is identical to an "RST interrupt_num" instruction. state->pc = 8 * interrupt_num; } 

Le code de la plateforme doit implémenter une minuterie que nous pouvons appeler (pour l'instant, je l'appelle simplement time ()). Le code machine l'utilisera pour transmettre une interruption à l'émulateur 8080. Dans le code machine, lorsque le temporisateur expire, j'appelle GenerateInterrupt:

  while (!done) { Emulate8080Op(state); if ( time() - lastInterrupt > 1.0/60.0) //1/60 second has elapsed { //only do an interrupt if they are enabled if (state->int_enable) { GenerateInterrupt(state, 2); //interrupt 2 //Save the time we did this lastInterrupt = time(); } } } 

Il y a quelques détails sur la façon dont le 8080 gère réellement les interruptions, que nous n'émulerons pas. Je pense qu'un tel traitement sera suffisant pour nos besoins.

Source: https://habr.com/ru/post/fr419143/


All Articles