Navigation

Tutoriel précédent : exemples avec quelques instructions   Sommaire    

II. Introduction

Maintenant que notre émulateur donne des résultats palpables, nous allons nous occuper des entrées utilisateur et du son.

III. Les entrées utilisateur

L'entrée est faite avec un clavier qui possède 16 touches allant de 0 à F. Les touches « 8 », « 4 », « 6 » et « 2 » sont généralement utilisées pour l'entrée directionnelle.
Esquisse clavier Chip 8

Pour simuler ce clavier, nous allons utiliser le pavé numérique et la touche de direction droite. Ce choix est purement subjectif. Vous pourrez le modifier à votre guise si vous avez bien compris.

Correspondance clavier Chip 8 avec clavier ordinateur

Il nous faudra une variable pour connaître l'état des boutons (pressés ou non). Pour ma part, j'ai utilisé un tableau de 16 Uint8 pour nos 16 boutons.

Et pour gérer les changements d'état de nos touches (pressé ou relâché), nous allons utiliser les événements « KEYDOWN » et « KEYUP » de SDL. On aura quelque chose qui ressemblera à ceci (les variables touches seront insérées dans la structure CPU).

 
Sélectionnez
Uint8 listen() 
{ 

Uint8 continuer=1; 
 while( SDL_PollEvent(&event)) 
   { 
                    switch(event.type) 
                    { 
                        case SDL_QUIT: {continuer = 0;break;} 
                        case SDL_KEYDOWN:{ 
                                            switch(event.key.keysym.sym) 
                                            { 
                                                case SDLK_KP0:{ cpu.touche[0x0]=1;break;} 
                                                case SDLK_KP7:{ cpu.touche[0x1]=1;break;} 
                                                case SDLK_KP8:{ cpu.touche[0x2]=1;break;} 
                                                /* 
                                                  Et toutes les autres touches 
                                                */ 
                                                default:{ break;} 
                                            } ;break;} 
                            case SDL_KEYUP:{ 
                                            switch(event.key.keysym.sym) 
                                            { 
                                                case SDLK_KP0:{ cpu.touche[0x0]=0;break;} 
                                                case SDLK_KP7:{ cpu.touche[0x1]=0;break;} 
                                                case SDLK_KP8:{ cpu.touche[0x2]=0;break;} 
                                                 /* 
                                                   Et toutes les autres touches 
                                                 */ 
                                                default:{ break;} 
                                            } ;break;} 

                        default:{ break;} 
                    } 

     } 
        
return continuer; 
}

J'ai adopté la convention suivante : « 0 » signifie que la touche est relâchée, « 1 » que la touche est pressée.

III-A. Traitement des opcodes

On peut maintenant revenir sur tous les opcodes qui traitent les entrées utilisateur.

EXA1 Saute l'instruction suivante si la clé stockée dans VX n'est pas pressée.
EX9E Saute l'instruction suivante si la clé stockée dans VX est pressée.

Ces deux instructions sont relativement simples. La variable VX contiendra un nombre variant de 0 à 15 (pour nos 16 touches). Il faudra vérifier la valeur de touche[VX] et agir en conséquence. Comme nous l'avons déjà vu précédemment, pour sauter une instruction, il suffit d'incrémenter pc de 2.

 
Sélectionnez
//EX9E saute l'instruction suivante si la clé stockée dans VX est pressée. 
if(cpu.touche[V[b3]]==1) //1 = pressé ; 0 = relâché 
{ 
   cpu.pc+=2; 
} 

//EXA1 saute l'instruction suivante si la clé stockée dans VX n'est pas pressée. 
if(cpu.touche[V[b3]]==0) //1 = pressé ; 0 = relâché 
{ 
  cpu.pc+=2; 
}

Et pour finir avec le clavier, on a :

FX0A L'appui sur une touche est attendu, puis la valeur correspondante est stockée dans VX.

Pour attendre l'appui sur une touche, on peut utiliser la fonction SDL_WaitEvent. Cette fonction s'exécute tant qu'aucune touche du jeu n'est appuyée. Si une touche quelconque est pressée, on inscrit sa valeur dans VX.

Voici le code correspondant :

 
Sélectionnez
Uint8 attendAppui(Uint8 b3) 
{ 
    Uint8 attend=1,continuer=1; 

    while(attend) 
    { 
        SDL_WaitEvent(&event); 

            switch(event.type) 
            { 
                 case SDL_QUIT:{ continuer=0; attend=0; break;} 

                case SDL_KEYDOWN:{ 

                    switch(event.key.keysym.sym) 
                    { 

                            case SDLK_KP0:{ cpu.V[b3]=0x0;cpu.touche[0x0]=1;attend=0;break;} 
                            case SDLK_KP7:{ cpu.V[b3]=0x1;cpu.touche[0x1]=1;attend=0;break;} 
                            case SDLK_KP8:{ cpu.V[b3]=0x2;cpu.touche[0x2]=1;attend=0;break;} 
                            /* Et le reste des touches */ 
                            default:{ break;} 
                    } break;} 

          default:{ break;} 
      } 
    } 

 return continuer; 
}

J'ai inséré quelques attributs supplémentaires, car je ne voulais pas que l'utilisateur n'ait pas la possibilité de quitter l'émulateur lorsque la fonction est en cours d'exécution. Mais si vous avez compris le principe, le code ne devrait pas poser de problème.

III-B. Testons le tout

Vous pouvez maintenant lancer un jeu comme « Breakout », par exemple. Si tout se passe bien, vous devriez obtenir quelque chose ressemblant à ceci :

Test des contrôles sur Breakout sur Chip 8

IV. Le son

Cette minuterie est utilisée pour les effets sonores. Lorsque sa valeur est différente de zéro, un signal sonore est émis.

Le système sonore de la Chip 8 est relativement simple. Nous avions déjà déclaré notre minuterie sonore ; il suffit juste de poser une condition sur la variable compteurSon et de jouer un bip sonore, de préférence si sa valeur est différente de zéro. Je ne vais pas détailler les méthodes pour charger un son. Personnellement, j'utilise SDL_Mixer.

 
Sélectionnez
if(cpu.compteurSon!=0) 
 { 
    Mix_PlayChannel(0, son, 0); //permet de jouer le bip sonore 
    cpu.compteurSon=0; 
 } 
//Rien de plus simple ^_^

Pour ceux qui utilisent des bibliothèques haut niveau, il arrivera un moment où émuler le son deviendra impossible. N'hésitez donc pas à faire des recherches sur le son numérique en général pour vos futurs émulateurs.

Si toutes les caractéristiques essentielles ont été implémentées, il ne reste plus qu'à mettre en place l'interface homme-machine et apporter d'éventuelles améliorations à notre émulateur.

V. À vous de jouer !

V-A. Améliorations

Pour finir notre émulateur, j'ai rajouté quelques fonctionnalités, à savoir :

  • faire une pause ;
  • redémarrer un jeu.

Je ne détaillerai pas les actions effectuées, mais vous pouvez ajouter plein d'autres choses à votre émulateur : créer un système de sauvegarde, faire un écran de taille réglable, élaborer une interface pour faciliter le lancement des jeux, etc.

Voici le code final de notre projet.

pixel.h
Sélectionnez
#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
Sélectionnez
#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
Sélectionnez
#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 
        Uint8 touche[16]; //pour stocker l'état des touches 
    } 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(); 
void chargerFont(); 
void dessinerEcran(Uint8,Uint8,Uint8); 
void reset(); 

Uint16 recupererOpcode(); 
Uint8 interpreterOpcode(Uint16); 
Uint8 recupererAction(Uint16); 
Uint8 attendAppui(Uint8); 

#endif
cpu.c
Sélectionnez
#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.touche[i]=0; 
    } 

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

    initialiserJump(); 
} 

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

    cpu.pc=ADRESSEDEBUT; 
    cpu.nbrsaut=0; 
    cpu.compteurJeu=0; 
    cpu.compteurSon=0; 
    cpu.I=0; 
    initialiserPixel(); 
    updateEcran(); 

} 


void initialiserJump() 
{ 

  jp.masque[0]= 0x0000; jp.id[0]=0x0FFF;          /* 0NNN */ 
  jp.masque[1]= 0xFFFF; jp.id[1]=0x00E0;          /* 00E0 */ 
  jp.masque[2]= 0xFFFF; jp.id[2]=0x00EE;          /* 00EE */ 
  jp.masque[3]= 0xF000; jp.id[3]=0x1000;          /* 1NNN */ 
  jp.masque[4]= 0xF000; jp.id[4]=0x2000;          /* 2NNN */ 
  jp.masque[5]= 0xF000; jp.id[5]=0x3000;          /* 3XNN */ 
  jp.masque[6]= 0xF000; jp.id[6]=0x4000;          /* 4XNN */ 
  jp.masque[7]= 0xF00F; jp.id[7]=0x5000;          /* 5XY0 */ 
  jp.masque[8]= 0xF000; jp.id[8]=0x6000;          /* 6XNN */ 
  jp.masque[9]= 0xF000; jp.id[9]=0x7000;          /* 7XNN */ 
  jp.masque[10]= 0xF00F; jp.id[10]=0x8000;          /* 8XY0 */ 
  jp.masque[11]= 0xF00F; jp.id[11]=0x8001;          /* 8XY1 */ 
  jp.masque[12]= 0xF00F; jp.id[12]=0x8002;          /* 8XY2 */ 
  jp.masque[13]= 0xF00F; jp.id[13]=0x8003;          /* BXY3 */ 
  jp.masque[14]= 0xF00F; jp.id[14]=0x8004;          /* 8XY4 */ 
  jp.masque[15]= 0xF00F; jp.id[15]=0x8005;          /* 8XY5 */ 
  jp.masque[16]= 0xF00F; jp.id[16]=0x8006;          /* 8XY6 */ 
  jp.masque[17]= 0xF00F; jp.id[17]=0x8007;          /* 8XY7 */ 
  jp.masque[18]= 0xF00F; jp.id[18]=0x800E;          /* 8XYE */ 
  jp.masque[19]= 0xF00F; jp.id[19]=0x9000;          /* 9XY0 */ 
  jp.masque[20]= 0xF000; jp.id[20]=0xA000;          /* ANNN */ 
  jp.masque[21]= 0xF000; jp.id[21]=0xB000;          /* BNNN */ 
  jp.masque[22]= 0xF000; jp.id[22]=0xC000;          /* CXNN */ 
  jp.masque[23]= 0xF000; jp.id[23]=0xD000;          /* DXYN */ 
  jp.masque[24]= 0xF0FF; jp.id[24]=0xE09E;          /* EX9E */ 
  jp.masque[25]= 0xF0FF; jp.id[25]=0xE0A1;          /* EXA1 */ 
  jp.masque[26]= 0xF0FF; jp.id[26]=0xF007;          /* FX07 */ 
  jp.masque[27]= 0xF0FF; jp.id[27]=0xF00A;          /* FX0A */ 
  jp.masque[28]= 0xF0FF; jp.id[28]=0xF015;          /* FX15 */ 
  jp.masque[29]= 0xF0FF; jp.id[29]=0xF018;          /* FX18 */ 
  jp.masque[30]= 0xF0FF; jp.id[30]=0xF01E;          /* FX1E */ 
  jp.masque[31]= 0xF0FF; jp.id[31]=0xF029;          /* FX29 */ 
  jp.masque[32]= 0xF0FF; jp.id[32]=0xF033;          /* FX33 */ 
  jp.masque[33]= 0xF0FF; jp.id[33]=0xF055;          /* FX55 */ 
  jp.masque[34]= 0xF0FF; jp.id[34]=0xF065;          /* FX65 */ 

} 

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 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 interpreterOpcode(Uint16 opcode) 
{ 
    Uint8 continuer=1; 
    Uint8 b4,b3,b2,b1; 

    b3=(opcode&(0x0F00))>>8;  //on prend les 4 bits représentant X 
    b2=(opcode&(0x00F0))>>4;  //idem pour Y 
    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   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. 
                if(cpu.touche[cpu.V[b3]]==1)//1 pressé, 0 relaché 
                { 
                    cpu.pc+=2; 
                } 

                break; 
            } 
    case 25:{ 
            //EXA1 saute l'instruction suivante si la clé stockée dans VX n'est pas pressée. 
                if(cpu.touche[cpu.V[b3]]==0)//1 pressé, 0 relaché 
                { 
                    cpu.pc+=2; 
                } 

                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. 
                continuer=attendAppui(b3); 

                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 
    return continuer; 
} 



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 
                            } 


                        } 
            } 
        } 

} 

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 

    //OUF ! 

} 


Uint8 attendAppui(Uint8 b3) 
{ 
    Uint8 attend=1,continuer=1; 

    while(attend) 
    { 
        SDL_WaitEvent(&event); 

            switch(event.type) 
            { 
                case SDL_QUIT:{ continuer=0;attend=0; break;} 

                case SDL_KEYDOWN:{ 

                    switch(event.key.keysym.sym) 
                    { 

                            case SDLK_KP0:{ cpu.V[b3]=0x0; cpu.touche[0x0]=1; attend=0;break;} 
                            case SDLK_KP7:{ cpu.V[b3]=0x1; cpu.touche[0x1]=1; attend=0;break;} 
                            case SDLK_KP8:{ cpu.V[b3]=0x2; cpu.touche[0x2]=1; attend=0;break;} 
                            case SDLK_KP9:{ cpu.V[b3]=0x3; cpu.touche[0x3]=1; attend=0;break;} 
                            case SDLK_KP4:{ cpu.V[b3]=0x4; cpu.touche[0x4]=1; attend=0;break;} 
                            case SDLK_KP5:{ cpu.V[b3]=0x5; cpu.touche[0x5]=1; attend=0;break;} 
                            case SDLK_KP6:{ cpu.V[b3]=0x6; cpu.touche[0x6]=1; attend=0;break;} 
                            case SDLK_KP1:{ cpu.V[b3]=0x7; cpu.touche[0x7]=1; attend=0;break;} 
                            case SDLK_KP2:{ cpu.V[b3]=0x8; cpu.touche[0x8]=1; attend=0;break;} 
                            case SDLK_KP3:{ cpu.V[b3]=0x9; cpu.touche[0x9]=1; attend=0;break;} 
                            case SDLK_RIGHT:{        cpu.V[b3]=0xA;       cpu.touche[0xA]=1;  attend=0;break;} 
                            case SDLK_KP_PERIOD:{    cpu.V[b3]=0xB;     cpu.touche[0xB]=1;  attend=0;break;} 
                            case SDLK_KP_MULTIPLY:{  cpu.V[b3]=0xC;     cpu.touche[0xC]=1;  attend=0;break;} 
                            case SDLK_KP_MINUS:{     cpu.V[b3]=0xD;     cpu.touche[0xD]=1;  attend=0;break;} 
                            case SDLK_KP_PLUS:{      cpu.V[b3]=0xE;     cpu.touche[0xE]=1;  attend=0;break;} 
                            case SDLK_KP_ENTER:{     cpu.V[b3]=0xF;     cpu.touche[0xF]=1;  attend=0;break;} 
                            default:{ break;} 
                    } break;} 

          default:{ break;} 
      } 
    } 


 return continuer; 
}
main.c
Sélectionnez
#include <SDL/SDL_mixer.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(); 

Mix_Chunk *son; 


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

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

    son=NULL; 

    son = Mix_LoadWAV("SON/beep.wav"); 
     
        if(son==NULL) 
        { 
            fprintf(stderr,"Problème avec le son"); 
            exit(EXIT_FAILURE); 
        } 

    if(argc>=2) //Permet de charger un jeu en ligne de commande ou en le plaçant dans l'exécutable 
    { 
      demarrer=chargerJeu(argv[1]); 
    } 

    if(demarrer==1) 
    { 
    do 
    { 
       continuer=listen(); //pour les entrées utilisateur 

       for(compteur=0;compteur<VITESSECPU && continuer==1;compteur++)  //Si continuer=0, on quitte l'émulateur 
       { 
        continuer=interpreterOpcode(recupererOpcode()); 
       } 

       if(cpu.compteurSon!=0) 
       { 
        Mix_PlayChannel(0, son, 0); 
        cpu.compteurSon=0; 
       } 

            updateEcran(); 
            decompter(); 
             
           SDL_Delay(FPS); //une pause de 16 ms 
           
    }while(continuer==1); 

    } 

return EXIT_SUCCESS; 
} 


void initialiserSDL() 
{ 
    atexit(quitterSDL); 

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

     if(Mix_OpenAudio(22050, MIX_DEFAULT_FORMAT, MIX_DEFAULT_CHANNELS, 1024) == -1) //Initialisation de Mixer 
     { 
        fprintf(stderr,"Problème d'initialisation de SDL_MIXER: %s",Mix_GetError()); 
        exit(EXIT_FAILURE); 
     } 
     Mix_AllocateChannels(1); 

} 


void quitterSDL() 
{ 
    SDL_FreeSurface(carre[0]); 
    SDL_FreeSurface(carre[1]); 
    Mix_FreeChunk(son); 
    Mix_CloseAudio(); 
    SDL_Quit(); 
} 

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:{ 
                                    switch(event.key.keysym.sym) 
                                    { 
                                        case SDLK_KP0:{ cpu.touche[0x0]=1;break;} 
                                        case SDLK_KP7:{ cpu.touche[0x1]=1;break;} 
                                        case SDLK_KP8:{ cpu.touche[0x2]=1;break;} 
                                        case SDLK_KP9:{ cpu.touche[0x3]=1;break;} 
                                        case SDLK_KP4:{ cpu.touche[0x4]=1;break;} 
                                        case SDLK_KP5:{ cpu.touche[0x5]=1;break;} 
                                        case SDLK_KP6:{ cpu.touche[0x6]=1;break;} 
                                        case SDLK_KP1:{ cpu.touche[0x7]=1;break;} 
                                        case SDLK_KP2:{ cpu.touche[0x8]=1;break;} 
                                        case SDLK_KP3:{ cpu.touche[0x9]=1;break;} 
                                        case SDLK_RIGHT:{ cpu.touche[0xA]=1;break;} 
                                        case SDLK_KP_PERIOD:{cpu.touche[0xB]=1;break;} 
                                        case SDLK_KP_MULTIPLY:{cpu.touche[0xC]=1;break;} 
                                        case SDLK_KP_MINUS:{cpu.touche[0xD]=1;break;} 
                                        case SDLK_KP_PLUS:{cpu.touche[0xE]=1;break;} 
                                        case SDLK_KP_ENTER:{cpu.touche[0xF]=1;break;} 
                                        case SDLK_p:{pause();break;} 
                                        case SDLK_r:{reset();break;} 
                                        default:{ break;} 
                                    }                                      
                    break;}         
                case SDL_KEYUP:{ 
                                    switch(event.key.keysym.sym) 
                                    { 
                                        case SDLK_KP0:{ cpu.touche[0x0]=0;break;} 
                                        case SDLK_KP7:{ cpu.touche[0x1]=0;break;} 
                                        case SDLK_KP8:{ cpu.touche[0x2]=0;break;} 
                                        case SDLK_KP9:{ cpu.touche[0x3]=0;break;} 
                                        case SDLK_KP4:{ cpu.touche[0x4]=0;break;} 
                                        case SDLK_KP5:{ cpu.touche[0x5]=0;break;} 
                                        case SDLK_KP6:{ cpu.touche[0x6]=0;break;} 
                                        case SDLK_KP1:{ cpu.touche[0x7]=0;break;} 
                                        case SDLK_KP2:{ cpu.touche[0x8]=0;break;} 
                                        case SDLK_KP3:{ cpu.touche[0x9]=0;break;} 
                                        case SDLK_RIGHT:{ cpu.touche[0xA]=0;break;} 
                                        case SDLK_KP_PERIOD:{cpu.touche[0xB]=0;break;} 
                                        case SDLK_KP_MULTIPLY:{cpu.touche[0xC]=0;break;} 
                                        case SDLK_KP_MINUS:{cpu.touche[0xD]=0;break;} 
                                        case SDLK_KP_PLUS:{cpu.touche[0xE]=0;break;} 
                                        case SDLK_KP_ENTER:{cpu.touche[0xF]=0;break;} 
                                        default:{ break;} 
                                    } 
                break;} 
                                 
                        default:{ break;} 
            } 
      } 
     
 return continuer; 
} 


void pause() 
{ 
    Uint8 continuer=1; 
     
    do 
    { 
        SDL_WaitEvent(&event); 

        switch(event.type) 
         { 
             case SDL_QUIT: 
             
                    continuer=0; 
                    break; 
                     
             case SDL_KEYDOWN: 
             
                        if(event.key.keysym.sym==SDLK_p) 
                           continuer=0; 
                    break; 
                     
             default:   break; 
         } 
    }while(continuer==1); 
     
    SDL_Delay(200); //on fait une petite pause pour ne pas prendre le joueur au dépourvu 
}

Le code final diffère légèrement des codes vus plus haut car j'ai moi-même ajouté de nouvelles fonctionnalités à mon émulateur. Vous verrez de nouvelles variables et fonctions qui ne sont pas indispensables. Essayez d'écrire votre propre code afin de vous améliorer.

Si vous désirez tester le code, il suffit de créer un dossier dans le même emplacement que l'exécutable. Nommez-le « SON » et placez-y un fichier son au format WAV. Le fichier son devra être nommé « beep ».

V-B. La compatibilité

Si vous testez certains jeux, notamment blinky et blitz, vous verrez que l'émulateur ne peut pas les faire fonctionner. C'est un problème de compatibilité qui se pose.

Je le dis et je le répète, les informations sur lesquelles nous nous basons pour construire notre émulateur ne sont pas unanimes. Certains jeux utilisent donc vraisemblablement des propriétés que nous n'avons pas implémentées.

Mais en bidouillant, j'ai remarqué que le jeu blitz boguait à cause du modulo dans la fonction de dessin sur l'écran. Donc, pour ce jeu, tout ce qui sort de l'écran ne doit pas être redessiné.

De même pour blinky, il faut augmenter la vitesse de jeu pour obtenir un bon rendu ; d'où l'intérêt de développer un émulateur configurable.

Je ne laisse pas de fichier .zip à télécharger pour la bonne et simple raison qu'un émulateur Chip 8, ce n'est pas ce qui manque, et vous êtes censés programmer le vôtre.

V-C. À suivre

Vous n'avez là qu'une petite esquisse de l'émulation console. Plusieurs notions n'ont pas été abordées puisque n'étant pas utilisées par la Chip 8. En vous souhaitant une bonne continuation, je vous donne quelques pistes à suivre…

V-C-1. Le son

L'émulation du son n'est pas aussi facile qu'on pourrait l'imaginer. La Chip 8 est un mauvais exemple dans ce domaine. Gardez en tête qu'il faut, dans la plupart des cas, générer le son grâce aux instructions du jeu sans faire appel à une ressource externe.

V-C-2. Les cycles

Dans notre bloc switch, nous avons défini le même temps d'exécution pour tous les opcodes ; toutefois, certaines consoles introduisent la notion de cycle d'horloge.

En effet, il se peut que certaines instructions prennent plus de temps à être exécutées que d'autres. Il faudra dans ce cas trouver des astuces afin d'assurer une émulation optimale.

V-C-3. Débogueur

Le nombre d'instructions de la Chip 8 est relativement faible. C'est d'ailleurs ce qui a motivé mon choix de la traiter. Pour les nouvelles consoles (même des anciennes), le nombre d'instructions est énorme, il sera donc utile de créer un débogueur pour voir quels sont les opcodes qui ne fonctionnent pas comme vous le souhaitez. Cela vous facilitera grandement la tâche.

V-C-4. Optimisation

Certaines consoles sont cadencées à une fréquence tellement élevée qu'une simple approche avec un bloc switch ne pourrait satisfaire. Il existe une méthode appelée recompilation dynamique (dynarec) qui permet de contourner cet obstacle. Cette méthode, bien qu'un peu compliquée, offre des performances inégalables !

V-D. Plus loin

Vous avez fini le tutoriel et vous en voulez encore plus ?

La Chip 8 a un descendant appelé SuperChip 8. Vous pourrez facilement trouver tout ce qu'il vous faut pour l'émuler.

D'ailleurs, la SuperChip 8 partage beaucoup d'instructions identiques avec la Chip 8, vous pourrez donc mettre à jour votre émulateur Chip 8 avec seulement quelques modifications.

La Superchip 8 a aussi un descendant appelé Mégachip 8.

Voici un système tout nouveau, CHIP 16, qui est conçu spécialement pour les débutants en émulation. Ce n'est pas mal pour un second émulateur. Le système est en couleur et est très agréable. Toutes les informations qu'il faut pour l'implémenter sont disponibles. Vous pourrez donc espérer une compatibilité de 100 %.

Nous voici à la fin de notre premier épisode. Vous venez de faire vos premiers pas dans le monde de l'émulation console et j'espère que ce tutoriel vous a été utile.

Merci de m'avoir lu, je vous donne rendez-vous bientôt pour une nouvelle aventure.

Si vous regroupez toutes les citations utilisées, vous retrouverez la présentation entière de Wikipédia. Eh oui, on vient de traduire le document en langage machine. Donc, lorsque je disais :

C'est grâce à ce document que nous allons programmer notre émulateur ; nous allons le traduire en langage machine.

… j'avais entièrement raison et vous pouvez en témoigner.

Vous voilà fin prêts pour vous aventurer dans le monde de l'émulation console.

Ce tutoriel, loin d'être exhaustif, ne représente qu'un aperçu de ce vaste domaine. Après cette initiation, vous êtes en mesure de programmer des émulateurs bien plus complexes du point de vue architectural sans problème.

Comment programmer un émulateur ?

Vous devez me dire sans hésiter qu'il suffit de :

  • trouver les caractéristiques de la machine ;
  • traduire le tout dans le langage de programmation de votre choix.

Mais j'insiste : pour réaliser un émulateur Sony Next Generation Portable (NGP) ou Nintendo 3DS (ou même PS3 ou XBOX 360 pour les plus téméraires), il faudra creuser un peu plus.

Navigation

Tutoriel précédent : exemples avec quelques instructions   Sommaire