Programmer un émulateur

Chapitre 5 : Exemples avec quelques instructions

BestCoder

Maintenant que le terrain a été bien préparé, il ne nous reste plus qu'à simuler les instructions de calcul une par une. Si vous avez suivi jusque-là, vous n'aurez aucun souci puisque cette partie est la plus facile.

Commentez

- Titre : Programmer un émulateur

- Auteur : BestCoder

- Parution : 1 avril 2014

- Licence : Le contenu de cet article est rédigé par BestCoder et est mis à disposition selon les termes de la Licence Creative Commons Attribution - Pas d'Utilisation Commerciale - Partage dans les Mêmes Conditions 3.0 non transposé. Les logos Developpez.com, en-tête, pied de page, css, et look & feel de l'article sont Copyright ® 2013 Developpez.com.

Sommaire

- Navigation

- II. Introduction

- III. Divers

- III-A. 00E0

- III-B. 1NNN

- III-C. 2NNN

- III-D. 00EE

- III-E. 3XNN

- III-F. 8XY0

- III-G. CXNN

- III-H. 8XY4

- III-I. 8XY7

- III-J. FX33

- IV. Le mode de dessin intégré

- IV-A. FX29

- V. Charger un jeu

- V-A. Quelques explications

- Navigation

Navigation

Tutoriel précédent : simulation des instructions

Sommaire

Tutoriel suivant : l'interface homme-machine

II - Introduction

Maintenant que le terrain a été bien préparé, il ne nous reste plus qu'à simuler les instructions de calcul une par une. Si vous avez suivi jusque-là, vous n'aurez aucun souci puisque cette partie est la plus facile.

Pour effectuer les tests, vous n'avez pas besoin d'implémenter les opcodes qui traitent les entrées utilisateur et le son. On les verra plus tard.

III - Divers

III-A - 00E0

00E0

Efface l'écran.

Pour effacer l'écran, il suffit de mettre tous les pixels en noir. Et comme nous avions déjà défini une méthode pour modifier les pixels, il faudra juste l'appeler dans la bonne case du switch.

III-B - 1NNN

1NNN

Effectue un saut à l'adresse NNN.

La variable qui permet de pointer sur une adresse est le program counter : pc. Il faudra lui affecter la valeur NNN-2Philippe DUVAL2014-03-06T07:02:07Question du béotien de service :Au sens strict, ici, cela veut dire qu'on va modifier la valeur de pc par l'impact de la valeur NNN-2.Si c'est ça, c'est OK.Si on veut dire qu'on lui donne la valeur NNN-2, il faut alors écrire : « Il faudra lui affecter la valeur NNN-2 ».. Le « -2 » vient du fait qu'il faut aussi prendre en compte l'incrémentation de pc à la fin du bloc switch.

cpu.pc=(b3<<8)+(b2<<4)+b1; //on prend le nombre NNN (pour le saut)
cpu.pc-=2; //n'oublions pas le pc+=2 à la fin du bloc switch

III-C - 2NNN

2NNN

Exécute le sous-programme à l'adresse NNN.

Cette instruction est proche de la 1NNN. Mais dans ce cas-ci, il faudra récupérer l'ancienne valeur de pc afin d'y revenir après. La variable saut sera utilisée.

cpu.saut[cpu.nbrsaut]=cpu.pc; //on reste là où on était

if(cpu.nbrsaut<15)
{
cpu.nbrsaut++;
}
//sinon, on a effectué trop de sauts

cpu.pc=(b3<<8)+(b2<<4)+b1; //on prend le nombre NNN (pour le saut)
cpu.pc-=2; //n'oublions pas le pc+=2 à la fin du block switch

III-D - 00EE

00EE

Retourne à partir d'un sous-programme.

La variable pc reçoit son ancienne valeur stockée dans saut.

if(cpu.nbrsaut>0)
{
cpu.nbrsaut--;
cpu.pc=cpu.saut[cpu.nbrsaut];
}
//sinon, on a effectué plus de retours que de sauts

III-E - 3XNN

3XNN

Saute l'instruction suivante si VX est égal à NN.

Pour sauter une instruction, il faut juste incrémenter pc de 2 dans la condition « VX est égal à NN ».

Avec l'incrémentation de pc à la fin du bloc switch, on se retrouvera à pc + 4.

if(cpu.V[b3]==((b2<<4)+b1))
{
cpu.pc+=2;
}

III-F - 8XY0

8XY0

Définit VX à la valeur de VY.

VX reçoit VY.

cpu.V[b3]=cpu.V[b2];

III-G - CXNN

CXNN

Définit VX à un nombre aléatoire inférieur à NN.

Avec (nombre_aleatoire)%(NN+1), le résultat ne pourra jamais dépasser NN.

cpu.V[b3]=(rand())%((b2<<4)+b1+1);

III-H - 8XY4

8XY4

Ajoute VY à VX. VF est mis à 1 quand il y a un dépassement de mémoire (carry), et à 0 quand il n'y en pas.

Je profite de cette instruction pour parler un peu de la notion de carry. Lors des calculs, il se peut que le résultat obtenu ne puisse pas être contenu dans notre variable. On parle alors de « dépassement » ou de « carry ». Par exemple, si nous devons effectuer la somme 0xF5+0x12, tout en étant sur 8 bits, le résultat est : 0x107. Cette valeur ne peut être contenue sur 8 bits, il y a donc un dépassement en mémoire (carry). Voici un petit schéma pour illustrer le tout :

Dans ce cas-ci, il y a un dépassement si le résultat de l'opération ne peut tenir sur 8 bits (taille des variables Vi). Il faut donc vérifier si la somme VX + VY est inférieure ou supérieure à 0xFF. 0XFF=255, c'est la valeur maximale que peut prendre un nombre non signé sur 8 bits.

if((cpu.V[b3]+cpu.V[b2])>0xFF)
{
cpu.V[0xF]=1; //V[15]
}
else
{
cpu.V[0xF]=0; //V[15]
}
cpu.V[b3]+=cpu.V[b2];

III-I - 8XY7

8XY7

VX = VY - VX. VF est mis à 0 quand il y a un emprunt et à 1 quand il n'y en a pas.

Il y a un emprunt si le résultat de l'opération est négatif. Il faut alors vérifier que VX > VY ou VY < VX, c'est vous qui voyez. Puisque nous avons déclaré nos variables comme étant non signées, les casts seront effectués pour nous.

if((cpu.V[b2]<cpu.V[b3]))     // /!\ VF est mis à 0 quand il y a emprunt !
{
cpu.V[0xF]=0; //cpu.V[15]
}
else
{
cpu.V[0xF]=1; //cpu.V[15]
}

cpu.V[b3]=cpu.V[b2]-cpu.V[b3];

III-J - FX33

FX33

Stocke dans la mémoire le code décimal représentant VX (dans I, I+1, I+2).

Le code décimal communément appelé BCD est la représentation d'un nombre en base 10.

Pour cette instruction, on doit stocker dans memoire[I] les centaines, dans memoire[I+1] les dizaines et dans memoire[I+2] les unités. Le nombre ne peut avoir de milliers ou plus puisqu'il est sur 8 bits. (La valeur maximale est donc 255 non signé.)

cpu.memoire[cpu.I]=(cpu.V[b3]-cpu.V[b3]%100)/100; //stocke les centaines
cpu.memoire[cpu.I+1]=(((cpu.V[b3]-cpu.V[b3]%10)/10)%10);//les dizaines
cpu.memoire[cpu.I+2]=cpu.V[b3]-cpu.memoire[cpu.I]*100-cpu.memoire[cpu.I+1]*10;//les unités

Pour ceux qui ont opté pour Java, les variables sont signées. C'est à vous de vérifier que vous ne dépassez pas la capacité des variables non signées ou que votre variable est négative en faisant des casts.

Par exemple, l'instruction nombre&=0xFFFF permet de maintenir la variable nombre sur 16 bits.

IV - Le mode de dessin intégré

IV-A - FX29

FX29

Définit I à l'emplacement du caractère stocké dans VX. Les caractères 0-F (en hexadécimal) sont représentés par une police 4x5.

Mais, c'est quoi cette histoire ?

Il est vrai que si on se limite à la description, on peut ne pas comprendre de quoi il s'agit (comme je l'avais dit, les documentations ne sont pas exhaustives sur tous les plans). Après un petit détour sur le Web, on voit que la Chip 8 possède en mémoire les caractères 0, 1, 2, 3, 4, 5, 6, 7, 8, A, B, C, D, E et F.

Comme pour le graphique, ces caractères sont codés en binaire et ont tous une largeur de 4 pixels et une longueur de 5 pixels.

Voici un petit schéma pour éclaircir les idées.

Tous ces caractères seront stockés dans memoire à partir de l'adresse 0. Si vous vous souvenez, les 512 premiers octets sont inutilisés. Chaque chiffre occupera cinq cases en mémoire.

Le caractère 0 occupera donc : memoire[0], memoire[1], memoire[2], memoire[3] et memoire[4].

De même, le caractère 1 occupera : memoire[5], memoire[6], memoire[7], memoire[8] et memoire[9].

void chargerFont()
{
cpu.memoire[0]=0xF0;cpu.memoire[1]=0x90;cpu.memoire[2]=0x90;cpu.memoire[3]=0x90; cpu.memoire[4]=0xF0; // O

cpu.memoire[5]=0x20;cpu.memoire[6]=0x60;cpu.memoire[7]=0x20;cpu.memoire[8]=0x20;cpu.memoire[9]=0x70; // 1

cpu.memoire[10]=0xF0;cpu.memoire[11]=0x10;cpu.memoire[12]=0xF0;cpu.memoire[13]=0x80; cpu.memoire[14]=0xF0; // 2

cpu.memoire[15]=0xF0;cpu.memoire[16]=0x10;cpu.memoire[17]=0xF0;cpu.memoire[18]=0x10;cpu.memoire[19]=0xF0; // 3

cpu.memoire[20]=0x90;cpu.memoire[21]=0x90;cpu.memoire[22]=0xF0;cpu.memoire[23]=0x10;cpu.memoire[24]=0x10; // 4

cpu.memoire[25]=0xF0;cpu.memoire[26]=0x80;cpu.memoire[27]=0xF0;cpu.memoire[28]=0x10;cpu.memoire[29]=0xF0; // 5

cpu.memoire[30]=0xF0;cpu.memoire[31]=0x80;cpu.memoire[32]=0xF0;cpu.memoire[33]=0x90;cpu.memoire[34]=0xF0; // 6

cpu.memoire[35]=0xF0;cpu.memoire[36]=0x10;cpu.memoire[37]=0x20;cpu.memoire[38]=0x40;cpu.memoire[39]=0x40; // 7

cpu.memoire[40]=0xF0;cpu.memoire[41]=0x90;cpu.memoire[42]=0xF0;cpu.memoire[43]=0x90;cpu.memoire[44]=0xF0; // 8

cpu.memoire[45]=0xF0;cpu.memoire[46]=0x90;cpu.memoire[47]=0xF0;cpu.memoire[48]=0x10;cpu.memoire[49]=0xF0; // 9

cpu.memoire[50]=0xF0;cpu.memoire[51]=0x90;cpu.memoire[52]=0xF0;cpu.memoire[53]=0x90;cpu.memoire[54]=0x90; // A

cpu.memoire[55]=0xE0;cpu.memoire[56]=0x90;cpu.memoire[57]=0xE0;cpu.memoire[58]=0x90;cpu.memoire[59]=0xE0; // B

cpu.memoire[60]=0xF0;cpu.memoire[61]=0x80;cpu.memoire[62]=0x80;cpu.memoire[63]=0x80;cpu.memoire[64]=0xF0; // C

cpu.memoire[65]=0xE0;cpu.memoire[66]=0x90;cpu.memoire[67]=0x90;cpu.memoire[68]=0x90;cpu.memoire[69]=0xE0; // D

cpu.memoire[70]=0xF0;cpu.memoire[71]=0x80;cpu.memoire[72]=0xF0;cpu.memoire[73]=0x80;cpu.memoire[74]=0xF0; // E

cpu.memoire[75]=0xF0;cpu.memoire[76]=0x80;cpu.memoire[77]=0xF0;cpu.memoire[78]=0x80;cpu.memoire[79]=0x80; // F

}

En définitive :

FX29

Définit I à l'emplacement du caractère stocké dans VX.

… revient à faire cpu.I =5*cpu.V[X];

Exemple :

- Si V[X] contient 1, I vaudra 5 qui est l'adresse de début de stockage du caractère 1.

- Si V[X] contient 2, I vaudra 10 qui est l'adresse de début de stockage du caractère 2.

Pour pratiquer un peu, je vous laisse finir les autres instructions. Cette phrase est courte mais ne soyez pas surpris si cela prend quelques heures.

V - Charger un jeu

Il ne reste plus qu'à charger nos jeux afin de faire nos premiers tests.

Les roms contiennent toutes les instructions à exécuter, il faudra donc charger le contenu du fichier binaire dans la mémoire.

Dans notre cas, les jeux sont chargés à partir de l'adresse 512 = 0x200.

Voici un lien pour télécharger ceux qui nous intéressent : jeux Chip8. J'ai programmé une rom pour vous afin de tester quelques opcodes. Vous pourrez l'utiliser pour déboguer votre émulateur. Le voici : BC_Chip8Test.

Uint8 chargerJeu(char *nomJeu)
{
FILE *jeu=NULL;
jeu=fopen(nomJeu,"rb"); /* Fichier binaire, donc rb */

if(jeu!=NULL)
{
fread(&cpu.memoire[ADRESSEDEBUT],sizeof(Uint8)*(TAILLEMEMOIRE-ADRESSEDEBUT), 1, jeu);
fclose(jeu);
return 1;
}
else
{
fprintf(stderr,"Problème d'ouverture du fichier");
return 0;
}

}

Nous sommes fin prêts pour faire nos premiers tests avec la BC-Chip8.

Voici le code mis à jour.

pixel.h

#ifndef PIXEL_H
#define PIXEL_H
#include <SDL/SDL.h>

#define NOIR  0
#define BLANC 1
#define l 64
#define L 32
#define DIMPIXEL 8
#define WIDTH   l*DIMPIXEL
#define HEIGHT  L*DIMPIXEL

typedef struct
{
SDL_Rect position; //regroupe l'abscisse et l'ordonnée
Uint32 couleur;   //comme son nom l'indique, c'est la couleur
} PIXEL;

SDL_Surface *ecran,*carre[2];
PIXEL pixel[l][L];
SDL_Event event;

void initialiserEcran() ;
void initialiserPixel() ;
void dessinerPixel(PIXEL pixel) ;
void effacerEcran() ;
void updateEcran() ;

#endif

pixel.c

#include "pixel.h"

void initialiserPixel()
{

Uint8 x=0,y=0;

for(x=0;x<l;x++)
{
for(y=0;y<L;y++)
{
pixel[x][y].position.x=x*DIMPIXEL;
pixel[x][y].position.y=y*DIMPIXEL;
pixel[x][y].couleur=NOIR;
}
}

}

void initialiserEcran()
{
ecran=NULL;
carre[0]=NULL;
carre[1]=NULL;

ecran=SDL_SetVideoMode(WIDTH,HEIGHT,32,SDL_HWSURFACE);
SDL_WM_SetCaption("BC-Chip8 By BestCoder",NULL);

if(ecran==NULL)
{
fprintf(stderr,"Erreur lors du chargement du mode vidéo %s",SDL_GetError());
exit(EXIT_FAILURE);
}

carre[0]=SDL_CreateRGBSurface(SDL_HWSURFACE,DIMPIXEL,DIMPIXEL,32,0,0,0,0); //le pixel noir
if(carre[0]==NULL)
{
fprintf(stderr,"Erreur lors du chargement de la surface %s",SDL_GetError());
exit(EXIT_FAILURE);
}
SDL_FillRect(carre[0],NULL,SDL_MapRGB(carre[0]->format,0x00,0x00,0x00)); //le pixel noir

carre[1]=SDL_CreateRGBSurface(SDL_HWSURFACE,DIMPIXEL,DIMPIXEL,32,0,0,0,0); //le pixel blanc
if(carre[1]==NULL)
{
fprintf(stderr,"Erreur lors du chargement de la surface %s",SDL_GetError());
exit(EXIT_FAILURE);
}
SDL_FillRect(carre[1],NULL,SDL_MapRGB(carre[1]->format,0xFF,0xFF,0xFF));  //le pixel blanc

}

void dessinerPixel(PIXEL pixel)
{
/* pixel.couleur peut prendre deux valeurs : 0, auquel cas on dessine le pixel en noir, ou 1, on dessine alors le pixel en blanc */
SDL_BlitSurface(carre[pixel.couleur],NULL,ecran,&pixel.position);
}

void effacerEcran()
{
//Pour effacer l'écran, on remet tous les pixels en noir
Uint8 x=0,y=0;
for(x=0;x<l;x++)
{
for(y=0;y<L;y++)
{
pixel[x][y].couleur=NOIR;
}
}

//on repeint l'écran en noir
SDL_FillRect(ecran,NULL,NOIR);
}

void updateEcran()
{
//On dessine tous les pixels à l'écran
Uint8 x=0,y=0;

for(x=0;x<l;x++)
{
for(y=0;y<L;y++)
{
dessinerPixel(pixel[x][y]);
}
}

SDL_Flip(ecran); //on affiche les modifications
}

cpu.h

#ifndef CPU_H
#define CPU_H
#include "pixel.h"

#define TAILLEMEMOIRE 4096
#define ADRESSEDEBUT 512
#define NBROPCODE 35

typedef struct
{
Uint8 memoire[TAILLEMEMOIRE];
Uint8 V[16]; //le registre
Uint16 I; //stocke une adresse mémoire ou dessinateur
Uint16 saut[16]; //pour gérer les sauts dans memoire, 16 au maximum
Uint8 nbrsaut; //stocke le nombre de sauts effectués pour ne pas dépasser 16
Uint8 compteurJeu; //compteur pour le graphisme (fréquence de rafraîchissement)
Uint8 compteurSon; //compteur pour le son
Uint16 pc; //pour parcourir le tableau memoire
} CPU;

CPU cpu;

typedef struct
{
Uint16 masque[NBROPCODE];   //la Chip 8 peut effectuer 35 opérations, chaque opération possédant son masque
Uint16 id[NBROPCODE];   //idem, chaque opération possède son propre identifiant

}JUMP;

JUMP jp;

void initialiserJump() ;
void initialiserCpu() ;
void decompter() ;
Uint16 recupererOpcode() ;
Uint8 recupererAction(Uint16) ;
void interpreterOpcode(Uint16) ;
void dessinerEcran(Uint8,Uint8,Uint8) ;
void chargerFont() ;
#endif

cpu.c

#include "cpu.h"

void initialiserCpu()
{
//On initialise le tout

Uint16 i=0;

for(i=0;i<TAILLEMEMOIRE;i++)
{
cpu.memoire[i]=0;
}

for(i=0;i<16;i++)
{
cpu.V[i]=0;
cpu.saut[i]=0;
}

cpu.pc=ADRESSEDEBUT;
cpu.nbrsaut=0;
cpu.compteurJeu=0;
cpu.compteurSon=0;
cpu.I=0;

initialiserJump(); //n'oubliez surtout pas cette fonction
}

void decompter()
{
if(cpu.compteurJeu>0)
cpu.compteurJeu--;

if(cpu.compteurSon>0)
cpu.compteurSon--;
}

Uint16 recupererOpcode()
{
return (cpu.memoire[cpu.pc]<<8)+cpu.memoire[cpu.pc+1];
}

Uint8 recupererAction(Uint16 opcode)
{
Uint8 action;
Uint16 resultat;

for(action=0; action<NBROPCODE;action++)
{
resultat= (jp.masque[action]&opcode);  /* On récupère les bits concernés par le test */

if(resultat == jp.id[action]) /* On a trouvé l'action à effectuer */
break; /* Plus la peine de continuer la boucle*/
}

return action;
}

void interpreterOpcode(Uint16 opcode)
{
Uint8 b4,b3,b2,b1;

b3=(opcode&(0x0F00))>>8;  //on prend les 4 bits suivants
b2=(opcode&(0x00F0))>>4;  //idem
b1=(opcode&(0x000F));     //idem

b4= recupererAction(opcode);

switch(b4)
{
case 0:{
//Cet opcode n'est pas implémenté.
break;
}
case 1:{
//00E0 efface l'écran.
effacerEcran();
break;
}

case 2:{
//00EE revient du saut.

if(cpu.nbrsaut>0)
{
cpu.nbrsaut--;
cpu.pc=cpu.saut[cpu.nbrsaut];
}
break;
}
case 3:{
//1NNN effectue un saut à l'adresse 1NNN.

cpu.pc=(b3<<8)+(b2<<4)+b1; //on prend le nombre NNN (pour le saut)
cpu.pc-=2; //on verra pourquoi à la fin

break;
}
case 4:{
//2NNN appelle le sous-programme en NNN, mais on revient ensuite.

cpu.saut[cpu.nbrsaut]=cpu.pc; //on reste là où on était

if(cpu.nbrsaut<15)
{
cpu.nbrsaut++;
}

cpu.pc=(b3<<8)+(b2<<4)+b1; //on prend le nombre NNN (pour le saut)
cpu.pc-=2; //on verra pourquoi à la fin

break;
}
case 5:{
//3XNN saute l'instruction suivante si VX est égal à NN.

if(cpu.V[b3]==((b2<<4)+b1))
{
cpu.pc+=2;
}

break;
}
case 6:{
//4XNN saute l'instruction suivante si VX et NN ne sont pas égaux.
if(cpu.V[b3]!=((b2<<4)+b1))
{
cpu.pc+=2;
}

break;
}
case 7:{
//5XY0 saute l'instruction suivante si VX et VY sont égaux.
if(cpu.V[b3]==cpu.V[b2])
{
cpu.pc+=2;
}

break;
}

case 8:{
//6XNN définit VX à NN.
cpu.V[b3]=(b2<<4)+b1;
break;
}
case 9:{
//7XNN ajoute NN à VX.
cpu.V[b3]+=(b2<<4)+b1;

break;
}
case 10:{
//8XY0 définit VX à la valeur de VY.
cpu.V[b3]=cpu.V[b2];

break;
}
case 11:{
//8XY1 définit VX à VX OR VY.
cpu.V[b3]=cpu.V[b3]|cpu.V[b2];

break;
}
case 12:{
//8XY2 définit VX à VX AND VY.
cpu.V[b3]=cpu.V[b3]&cpu.V[b2];

break;
}
case 13:{
//8XY3 définit VX à VX XOR VY.
cpu.V[b3]=cpu.V[b3]^cpu.V[b2];

break;
}
case 14:{
//8XY4 ajoute VY à VX. VF est mis à 1 quand il y a un dépassement de mémoire (carry), et à 0 quand il n'y en pas.
if((cpu.V[b3]+cpu.V[b2])>255)
{
cpu.V[0xF]=1; //cpu.V[15]
}
else
{
cpu.V[0xF]=0; //cpu.V[15]
}
cpu.V[b3]+=cpu.V[b2];

break;
}
case 15:{
//8XY5 VY est soustraite de VX. VF est mis à 0 quand il y a un emprunt, et à 1 quand il n'y a en pas.

if((cpu.V[b3]<cpu.V[b2]))
{
cpu.V[0xF]=0; //cpu.V[15]
}
else
{
cpu.V[0xF]=1; //cpu.V[15]
}
cpu.V[b3]-=cpu.V[b2];

break;
}
case 16:{
//8XY6 décale (shift) VX à droite de 1 bit. VF est fixé à la valeur du bit de poids faible de VX avant le décalage.
cpu.V[0xF]=(cpu.V[b3]&(0x01));
cpu.V[b3]=(cpu.V[b3]>>1);

break;
}
case 17:{
//8XY7 VX = VY - VX. VF est mis à 0 quand il y a un emprunt et à 1 quand il n'y en a pas.
if((cpu.V[b2]<cpu.V[b3]))
{
cpu.V[0xF]=0; //cpu.V[15]
}
else
{
cpu.V[0xF]=1; //cpu.V[15]
}
cpu.V[b3]=cpu.V[b2]-cpu.V[b3];

break;
}
case 18:{
//8XYE décale (shift) VX à gauche de 1 bit. VF est fixé à la valeur du bit de poids fort de VX avant le décalage.
cpu.V[0xF]=(cpu.V[b3]>>7);
cpu.V[b3]=(cpu.V[b3]<<1);

break;
}

case 19:{
//9XY0 saute l'instruction suivante si VX et VY ne sont pas égaux.
if(cpu.V[b3]!=cpu.V[b2])
{
cpu.pc+=2;
}

break;
}
case 20:{
//ANNN affecte NNN à I.

cpu.I=(b3<<8)+(b2<<4)+b1;

break;
}
case 21:{
//BNNN passe à l'adresse NNN + V0.

cpu.pc=(b3<<8)+(b2<<4)+b1+cpu.V[0];
cpu.pc-=2;

break;

}
case 22:{
//CXNN définit VX à un nombre aléatoire inférieur à NN.
cpu.V[b3]=(rand())%((b2<<4)+b1+1);

break;

}

case 23:{
//DXYN dessine un sprite aux coordonnées (VX, VY).

dessinerEcran(b1,b2,b3) ;

break;

}
case 24:{
//EX9E saute l'instruction suivante si la clé stockée dans VX est pressée.

break;
}
case 25:{
//EXA1 saute l'instruction suivante si la clé stockée dans VX n'est pas pressée.

break;
}

case 26:{
//FX07 définit VX à la valeur de la temporisation.
cpu.V[b3]=cpu.compteurJeu;

break;
}
case 27:{
//FX0A attend l'appui sur une touche et stocke ensuite la donnée dans VX.

break;
}

case 28:{
//FX15 définit la temporisation à VX.
cpu.compteurJeu=cpu.V[b3];

break;
}
case 29:{
//FX18 définit la minuterie sonore à VX.
cpu.compteurSon=cpu.V[b3];

break;
}
case 30:{
//FX1E ajoute VX à I. VF est mis à 1 quand il y a overflow (I+VX>0xFFF), et à 0 si tel n'est pas le cas.

if((cpu.I+cpu.V[b3])>0xFFF)
{
cpu.V[0xF]=1;
}
else
{
cpu.V[0xF]=0;
}
cpu.I+=cpu.V[b3];

break;
}

case 31:{
//FX29 définit I à l'emplacement du caractère stocké dans VX. Les caractères 0-F (en hexadécimal) sont représentés par une police 4x5.
cpu.I=cpu.V[b3]*5;

break;
}

case 32:{
//FX33 stocke dans la mémoire le code décimal représentant VX (dans I, I+1, I+2).

cpu.memoire[cpu.I]=(cpu.V[b3]-cpu.V[b3]%100)/100;
cpu.memoire[cpu.I+1]=(((cpu.V[b3]-cpu.V[b3]%10)/10)%10);
cpu.memoire[cpu.I+2]=cpu.V[b3]-cpu.memoire[cpu.I]*100-10*cpu.memoire[cpu.I+1];

break;
}
case 33:{
//FX55 stocke V0 à VX en mémoire à partir de l'adresse I.
Uint8 i=0;
for(i=0;i<=b3;i++)
{
cpu.memoire[cpu.I+i]=cpu.V[i];
}

break;
}
case 34:{
//FX65 remplit V0 à VX avec les valeurs de la mémoire à partir de l'adresse I.

Uint8 i=0;

for(i=0;i<=b3;i++)
{
cpu.V[i]=cpu.memoire[cpu.I+i];
}

break;
}

default: { //si ça arrive, il y un truc qui cloche

break;
}

}
cpu.pc+=2; //on passe au prochain opcode

}

void dessinerEcran(Uint8 b1,Uint8 b2, Uint8 b3)
{
Uint8 x=0,y=0,k=0,codage=0,j=0,decalage=0;
cpu.V[0xF]=0;

for(k=0;k<b1;k++)
{
codage=cpu.memoire[cpu.I+k]; //on récupère le codage de la ligne à dessiner

y=(cpu.V[b2]+k)%L; //on calcule l'ordonnée de la ligne à dessiner, on ne doit pas dépasser L

for(j=0,decalage=7;j<8;j++,decalage--)
{
x=(cpu.V[b3]+j)%l; //on calcule l'abscisse, on ne doit pas dépasser l

if(((codage)&(0x1<<decalage))!=0) //on récupère le bit correspondant
{   //si c'est blanc
if( pixel[x][y].couleur==BLANC) //le pixel était blanc
{
pixel[x][y].couleur=NOIR; //on l'éteint
cpu.V[0xF]=1; //il y a donc collusion

}
else //sinon
{
pixel[x][y].couleur=BLANC; //on l'allume
}

}
}
}
}

main.c

#include <SDL/SDL.h>
#include "cpu.h"

#define VITESSECPU 4 //nombre d'opérations par tour
#define FPS 16      //pour le rafraîchissement

void initialiserSDL();
void quitterSDL();
void pause();
Uint8 chargerJeu(char *);
Uint8 listen();

int main(int argc, char *argv[])
{
initialiserSDL() ;
initialiserEcran() ;
initialiserPixel() ;

Uint8 continuer=1,demarrer=0,compteur=0;

demarrer=chargerJeu("MAZE.ch8") ;

if(demarrer==1)
{
do
{
continuer=listen() ; //afin de pouvoir quitter l'émulateur

for(compteur=0;compteur<VITESSECPU;compteur++)
{
interpreterOpcode(recupererOpcode()) ;
}

updateEcran();
decompter();
SDL_Delay(FPS); //une pause de 16 ms

}while(continuer==1);

}

pause();

return EXIT_SUCCESS;
}

void initialiserSDL()
{
atexit(quitterSDL) ;

if(SDL_Init(SDL_INIT_VIDEO)==-1)
{
fprintf(stderr,"Erreur lors de l'initialisation de la SDL %s",SDL_GetError());
exit(EXIT_FAILURE) ;
}

}

void quitterSDL()
{

SDL_FreeSurface(carre[0]) ;
SDL_FreeSurface(carre[1]) ;
SDL_Quit() ;
}

void pause()
{

Uint8 continuer=1;

do
{
SDL_WaitEvent(&event) ;

switch(event.type)
{
case SDL_QUIT:
continuer=0;
break;
case SDL_KEYDOWN:
continuer=0;
break;
default: break;
}
}while(continuer==1);

}

Uint8 chargerJeu(char *nomJeu)
{
FILE *jeu=NULL;
jeu=fopen(nomJeu,"rb") ;

if(jeu!=NULL)
{
fread(&cpu.memoire[ADRESSEDEBUT],sizeof(Uint8)*(TAILLEMEMOIRE-ADRESSEDEBUT), 1, jeu) ;
fclose(jeu) ;
return 1 ;
}
else
{
fprintf(stderr,"Problème d'ouverture du fichier") ;
return 0;
}

}

Uint8 listen()
{

Uint8 continuer=1;
while(SDL_PollEvent(&event))
{
switch(event.type)
{
case SDL_QUIT: {continuer = 0;break;}
case SDL_KEYDOWN:{continuer=0 ;break;}

default:{ break;}
}
}
return continuer;
}

V-A - Quelques explications

Jetez un coup d'œil dans le main (et rien que le main). Vous pourrez remarquer une fonction listen(). Cette fonction n'a rien à voir avec l'émulation : elle nous permet de quitter la boucle principale.

Ensuite, nous avons :

for(compteur=0;compteur<VITESSECPU;compteur++) //on effectue quatre opérations
{
interpreterOpcode(recupererOpcode());
}

updateEcran();
decompter(); //les timers

SDL_Delay(FPS); //une pause de 16 ms

C'est ici que réside le code qui illustre la partie « Le cadencement du CPU et les FPS ».

Pour chaque tour de boucle, nous avons :

- les quatre opérations à effectuer grâce au for ;

- l'actualisation de l'écran grâce à updateEcran() ;

- et la pause de 16 ms grâce à SDL_Delay().

N'oubliez pas non plus d'appeler la fonction decompter(), qui nous permet de décompter à 60 Hz.

Ainsi, on respecte les spécifications que l'on s'est fixées au début. ;)

Après cela, on est aux anges.

Maze

Test/C8PIC

Ça fait beaucoup de bien d'obtenir ces résultats. Je dirais que l'émulation est vraiment magique.

Avec des décalages par-ci, des XOR par-là, on arrive à faire des choses incroyables !

Tout est bien qui finit bien. Notre émulateur donne des résultats plus que satisfaisants. Il ne nous reste plus qu'à implémenter le son et les entrées utilisateur, et ce sera terminé.

Navigation

Tutoriel précédent : simulation des instructions

Sommaire

Tutoriel suivant : l'interface homme-machine