Aller au menu - Aller au contenu

Icône Simulation des instructions

Mise à jour : 17/10/2011
Difficulté : Difficile Difficile Durée d'étude : 2 jours Creative Commons BY-NC-SA
632 visites depuis 7 jours, dont 96 sur ce chapitre classé 181/786
La Chip 8 ne sait exécuter que 35 opérations. o_O Si, si, vous avez bien entendu, ce sera aux programmeurs des jeux et applications de combiner les différentes opérations pour arriver à leurs fins. Nous nous contenterons d'implémenter les 35 opérations et notre émulateur sera opérationnel. C'est parti, nous abordons le dernier virage.
Sommaire du chapitre :
Icône du chapitre
Chapitre précédent Sommaire Chapitre suivant

Le cadencement du CPU et les FPS

Il y a deux choses qu'il faut absolument connaître quand on veut programmer un émulateur :
  • la vitesse d'exécution des instructions ou le cadencement de la machine ;
  • la période de rafraîchissement de l'écran ou le nombre de FPS.


Comment allons-nous procéder ?



Nous savons qu'une console de jeu est un système embarqué et ceux qui ont déjà programmé pour de l'embarqué (microprocesseur, microcontrôleur, etc.) savent que dans le code, il faut systématiquement une boucle infinie ou un système de timer car le code doit s'exécuter aussi longtemps que la console est en marche.

Code : C
1
2
3
4
5
while("Machine Fonctionne")
{
      regarder_ce_qu_il_faut_faire();
      Le_faire();
}


La fréquence d'exécution des instructions reste assez méconnue. Dans notre cas, nous allons utiliser une fréquence de 250 hertz. Cette fréquence nous donne une période de 4 ms (rappel mathématique : f=1/T avec T la période en secondes et f la fréquence en hertz).
Donc toutes les quatre millisecondes, nous devons effectuer une opération. Soit quatre opérations, toutes les seize millisecondes.

Pour les FPS aussi, la valeur est assez méconnue ( :-° ), on prendra donc la valeur la plus courante, à savoir 60. Soixante images par seconde, c'est une image toutes les 16,67 ms (1000 / 60 = 16,67). Un timer SDL aussi précis n'existe pas, on se limitera donc à une image toutes les 16 ms.

Récapitulatif



Il faut quatre opérations et une nouvelle image toutes les seize millisecondes. Donc la fonction updateEcran sera appelée après l'exécution de quatre opérations. Je gère le tout dans le main. Nous allons nous contenter de la boucle principale (le fameux while(continuer)) et des SDL_Delay.

Secret (cliquez pour afficher)
Code : C
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
#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() ;

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

    
    Uint8 continuer=1;

    do
    {
        //On effectuera quatre opérations ici
        
        updateEcran() ;
        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);

}


Ne testez surtout pas ce code. :lol:


Le choix de 250 Hz et 60 FPS est subjectif. Le problème est que les caractéristiques de la Chip 8 sont très méconnues, mais pour beaucoup d'autres consoles, vous trouverez facilement la fréquence exacte de cadencement du CPU. Et 250 Hz, c'est relativement facile à simuler. (Je sais, je suis paresseux. :D )

Lecture de l'opcode

Pour connaître l'action à effectuer, il faut lire dans la mémoire. Mais le problème est qu'elle est en octets (8 bits) et que les opcodessont, eux, de 16 bits.
Par exemple, pour l'opcode 8XY0 − qui, je le rappelle, est en hexadécimal − on a : 4 bits pour le « 0 » + 4 bits pour le « X » + 4 bits pour le « Y » + 4 bits pour le « 0 ». Ce qui nous donne 4 × 4 = 16 bits.
Si on effectue opcode=cpu.memoire[cpu.pc], il nous manquerait 8 bits. Il faut alors récupérer 16 bits dans la mémoire, à savoir cpu.memoire[cpu.pc] et cpu.memoire[cpu.pc+1].
Maintenant, il faut juste trouver un moyen pour les mettre « côte à côte » pour ne former qu'un unique nombre de 16 bits.

Les décalages (bit shift), ça vous dit quelque chose ?

Eh bien, ce sont eux qui vont nous faciliter la vie. Il suffit de décaler vers la gauche cpu.memoire[cpu.pc] de 8 bits et de faire la somme avec cpu.memoire[cpu.pc+1]. On aura ainsi un opcode de 16 bits.
Donc, on aura en définitive opcode=cpu.memoire[cpu.pc]<<8+cpu.memoire[cpu.pc+1].

Image utilisateur


La fonction pour effectuer ce calcul sera introduite dans cpu.h et cpu.c.

Code : C
1
2
3
4
5
6
Uint16 recupererOpcode() ; //prototype dans cpu.h

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


On vient de passer une étape, mais il reste encore deux ou trois détails et le tour sera joué.

Identification de l'instruction

Après avoir récupéré notre opcode, il ne reste plus qu'à l'interpréter. Par interpréter, il faut comprendre effectuer l'opération qui lui est associée.

Jetons un coup d'œil à nos opcodes (qui sont en hexadécimal) : 0NNN, 00E0, 00EE, 1NNN, 2NNN, 3XNN, 4XNN, 5XY0, 6XNN, 7XNN, 8XY0, 8XY1, 8XY2, 8XY3, 8XY4, 8XY5, etc. (ces valeurs proviennent du tableau de la partie Quelle machine émuler ?).

Tous les X, Y et N sont supposés inconnus. Pour connaître l'action à exécuter, il faut donc trouver un moyen d'identifier chaque opcode en ne tenant pas compte de ces valeurs. Pour ce faire, nous allons utiliser les opérations bits à bits combinées à une grosse amélioration de Pouet_forever.

Notre table de correspondance



Nous avons 35 opérations à effectuer. Pour chacune d'elles, j'ai donc associé un nombre unique compris entre 0 et 34.
Ensuite, suivant le nombre obtenu, on effectuera l'opération souhaitée en utilisant un bloc switch.

Trouver le masque et l'identifiant de l'opcode



Prenons l'opcode 0x8XY2 comme exemple. Pour l'identifier, on doit vérifier que les 4 bits de poids fort donnent 8 et les 4 bits de poids faible donnent 2.
Pour ce faire, on peut effectuer l'opération 0x8XY2 & 0xF00F qui nous donne 8002. À chaque fois que l'on effectue opcode_quelconque & 0xF00F et qu'on trouve 8002, il s'agit donc de 8XY2. Ingénieux, n'est-ce pas ? (Honte à moi, grand copieur de Pouet_Forever. :honte: ) L'opcode 8XY2 a donc pour masque 0xF00F et pour identifiant 0x8002.
Pour tous les autres opcodes, le principe reste le même. Exemples :

Opcode Masque Identifiant
00E0 FFFF 00E0
1NNN F000 1000
8XY3 F00F 8003
FX15 F0FF F015


Je stocke le tout dans une structure que j'ai appelée JUMP.

Code : C
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
//Dans cpu.h
#define NBROPCODE 35

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


//Dans cpu.c


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 */

}


Trouver l'opcode à interpréter



Le plus difficile est fait, il ne reste plus qu'à implémenter un algorithme nous permettant de retrouver le nombre associé à un opcode. Pour chaque opcode, il faut récupérer son identifiant en appliquant un & avec le masque et le comparer avec ceux de notre structure JUMP. Un exemple vaut mieux que mille discours.
Prenons le nombre 0x8475. Grâce à notre structure JUMP, nous devons être en mesure de retrouver 15, qui est le nombre associé aux opcodes 0x8XY5.

Comment ?

Il faut parcourir la structure JUMP pour trouver à quel indice i la condition 0x8475 & jp.masque[i]== jp.id[i] est vraie.
Pour ce cas-ci, i vaut 15, on a donc 0x8475 & jp.masque[15] == jp.id[15], soit 0x8475 & 0xF00F == 0x8005, ce qui est vrai. Pour toutes les autres valeurs de i, cette condition sera toujours fausse. Vérifiez par vous-mêmes pour voir. ^^
Voici le code de cet algorithme :
Code : C
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
//Dans cpu.h
Uint8 recupererAction(Uint16) ;


//Dans cpu.c


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, l'identifiant de l'opcode */

        if(resultat == jp.id[action]) /* On a trouvé l'action à effectuer */
           break; /* Plus la peine de continuer la boucle car la condition n'est vraie qu'une seule fois*/
    }

    return action;  //on renvoie l'indice de l'action à effectuer
}


Cas spécial : si vous regardez le masque et l'identifiant de l'opcode 0NNN, vous verrez que opcode&0x0000 − qui est toujours égal à 0x0000 − est toujours différent de 0xFFFF. En gros, on ne pourra jamais retrouver action=0. Comme je l'avais dit dans la partie de présentation de la Chip 8, « 0NNN appelle le programme de la RCA 1802 à l'adresse NNN. », cela ne nous intéresse donc pas.


À présent, pour simuler une instruction, il suffit de placer notre bloc switch.

Code : C
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
//Dans cpu.h

void interpreterOpcode(Uint16) ;


//Dans cpu.c

void interpreterOpcode(Uint16 opcode)
{
    Uint8 b4;

    b4= recupererAction(opcode);   //permet de connaître l'action à effectuer
 
    switch(b4)
    {
     case 0:{
               //Cet opcode n'est pas implémenté
                break;
              }
     case 1:{
               //00E0 : efface l'écran
                break;
               }

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

               
                break;
            }
     case 3:{ //1NNN : effectue un saut à l'adresse 1NNN
                break;
            }
     case 4:{
            //2NNN : appelle le sous-programme en NNN, mais on revient ensuite
                break;
            }
   
          // etc. jusqu'à 34


Il n'y a aucune condition à poser sur X, Y, N, NN et NNN, ces valeurs seront utilisées pour réaliser l'instruction souhaitée.


Par exemple, pour l'opcode 0x8XY2, on aura :

8XY2 Définit VX à VX AND VY.


Le code pour le réaliser est le suivant : il faut récupérer X et Y et effectuer V[X]=V[X]&V[Y]; //C'est trop simple, non ?!.

Pour récupérer les valeurs de X, Y, NN et NNN, il faut prendre les 12 bits de poids faible et les associer si l'opération en a besoin.

Code : C
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
//Ce code sera placé dans la fonction interpreterOpcode
    Uint8 b3,b2,b1;

    b3=(opcode&(0x0F00))>>8;  //on prend les 4 bits, b3 représente X
    b2=(opcode&(0x00F0))>>4;  //idem, b2 représente Y
    b1=(opcode&(0x000F));     //on prend les 4 bits de poids faible

   /*
      Pour obtenir NNN par exemple, il faut faire (b3<<8) + (b2<<4) + (b1)

   */


Passons maintenant au clou du spectacle : le graphique.

Retour sur le graphique

Mesdames, messieurs, veuillez attacher vos ceintures : nous allons entrer dans une zone de turbulences. :diable:
Pour dessiner à l'écran, la Chip 8 dispose d'un unique opcode (une seule instruction permet de dessiner à l'écran).

Citation
DXYN Dessine un sprite aux coordonnées (VX, VY).
Le sprite a une largeur de 8 pixels et une hauteur de pixels N.
Chaque rangée de 8 pixels est lue comme codée en binaire à partir de l'emplacement mémoire.
I ne change pas de valeur après l'exécution de cette instruction.


Citation
Les dessins sont établis à l'écran uniquement par l'intermédiaire de sprites, qui font 8 pixels de large et avec une hauteur qui peut varier de 1 à 15 pixels. Les sprites sont codés en binaire. Pour une valeur de 1, le pixel correspondant est allumé et pour une valeur 0, aucune opération n'est effectuée. Si un pixel d'un sprite est dessiné sur un pixel de l'écran déjà allumé, alors les deux pixels sont éteints. Le registre de retenue (VF) est mis à 1 à cet effet.


Image utilisateur


Comme vous le voyez sur l'image, chaque sprite peut être considéré comme un tableau à deux dimensions. Pour parcourir tout le sprite, il faudra donc deux boucles imbriquées.

Pour le codage des lignes, on récupère les valeurs dans la mémoire en commençant à l'adresse I. Si par exemple, on doit dessiner un sprite en (0,0) avec une hauteur de 3 et le codage memoire[I]=11010101, memoire[I+1]=00111100 et memoire[I+2]=11100011  //ces nombres sont en binaire, nous devrons obtenir :

Image utilisateur

Ensuite, si l'on souhaite dessiner un autre sprite en (0,0) avec une hauteur de 2 et le codage memoire[I]=01110101, memoire[I+1]=01110000, nous devrons obtenir :

Image utilisateur

Pour réaliser tout cela, il faut donc récupérer la couleur du pixel à dessiner, la comparer avec son ancienne valeur et agir en conséquence. On gardera en tête que pour « 0 », la couleur désirée est le noir, et pour « 1 », le blanc.
Voici le code C/SDL qui permet de réaliser tout ce bazar.

Secret (cliquez pour afficher)

Code : C
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
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
                            }


                        }

            }
        }

}



Ligne importante



(codage)&(0x1<<decalage)

Les décalages pointent encore le bout de leur nez. :p

Tout d'abord, il faut savoir que 0x1=00000001 en binaire sur 8 bits. L'instruction 0x1<<decalage permet de placer le « 1 » à l'endroit correspondant au codage de notre pixel.

Si par exemple, nous voulons dessiner le troisième pixel de la ligne, j vaut 2 (l'indice commence par 0) et decalage vaut 5. Donc 0x1<<decalage=00100000 en binaire sur 8 bits. Le « 1 » se place au troisième rang (il faut compter en partant de la gauche).
Donc, pour récupérer le bit correspondant à notre pixel, il suffit d'appliquer le and ou & et le tour est joué.

Je vous donne en prime tout le bloc switch de notre émulateur. Je vous préviens, il fait peur ce bloc. :lol:

Secret (cliquez pour afficher)

Code : C
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
void interpreter(Uint16 opcode)
{
    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));     //les 4 bits de poids faible

    b4= recupererAction(opcode);

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

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

               
                break;
            }
    case 3:{ //1NNN effectue un saut à l'adresse 1NNN.

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

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

                

                break;
            }
    case 6:{//4XNN saute l'instruction suivante si VX et NN ne sont pas égaux.
                

                break;
            }
    case 7:{
           //5XY0 saute l'instruction suivante si VX et VY sont égaux.
                
                break;
            }

    case 8:{
            //6XNN définit VX à NN.
               
                break;
            }
    case 9:{
                //7XNN ajoute NN à VX.
                

                break;
            }
    case 10:{
                //8XY0 définit VX à la valeur de VY.
                

                break;
            }
    case 11:{
                //8XY1 définit VX à VX OR VY.

                break;
            }
    case 12:{
                //8XY2 définit VX à VX AND VY.
                
                break;
            }
    case 13:{
                //8XY3 définit VX à VX XOR VY.
               
                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.
               

                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.
                

                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.
               

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

                break;
             }

    case 19:{

                //9XY0 saute l'instruction suivante si VX et VY ne sont pas égaux.
                
                break;
            }
    case 20:{
            //ANNN affecte NNN à I.

                

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

            

            break;

            }
    case 22:{

            //CXNN définit VX à un nombre aléatoire inférieur à NN.
           

            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.
               

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

                break;
            }


    case 28:{
                //FX15 définit la temporisation à VX.
               

                break;
            }
    case 29:{
                //FX18 définit la minuterie sonore à VX.
                

                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.


                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.
                

                break;
            }

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

               

                break;
            }
    case 33:{

                //FX55 stocke V0 à VX en mémoire à partir de l'adresse I.
               

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

               

                break;
            }

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

                    break;
             }

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



Je vous fais remarquer le pc+=2 à la fin du bloc switch. Après avoir exécuté l'opcode, il faut passer au suivant en incrémentant pc de 2 et non pas de 1. En effet, dans la fonction de récupération de l'opcode, on prend memoire[pc] et memoire[pc+1]


Image utilisateur
Le gros du travail vient d'être effectué. Il ne nous reste plus qu'à remplir les cases vides de notre switch avec les instructions qu'il faut, puis le tour est joué. Courage, on entrevoit le bout du tunnel ! :)
Chapitre précédent Sommaire Chapitre suivant

Partager

13 commentaires pour "Simulation des instructions"
Note moyenne : 3.89 / 4 (27 votes)
Pseudo Commentaire
Hors ligne BestCoder # Posté le 20/11/2011 à 11:02:26
Best Coder
Avatar

Avis : Très bon

Études : ESIGELEC

@alexisdim

Il y a plusieurs façon d'optimiser le code que j'ai donné comme exemple. Mais j'ai essayé de le faire le plus générique possible pour que les lecteurs puissent l'implémenter dans différents langages sans se soucier d'algorithmique.

@demonixis

C'est quelle routine d'affichage qui ne marche plus ?
 
Hors ligne demonixis # Posté le 20/11/2011 à 20:58:07
Avatar

Avis : Très bon
Flux RSS

Ville : Lyon
Pays : France métropolitaine

C'est DXY0 qui déconne un peut dans mon cas mais c'est peut être dû à du code plus bas niveau dans mon implémentation ;)



 
Hors ligne alexisdm # Posté le 21/11/2011 à 15:45:58

Citation : BestCoder
Il y a plusieurs façon d'optimiser le code que j'ai donné comme exemple. Mais j'ai essayé de le faire le plus générique possible pour que les lecteurs puissent l'implémenter dans différents langages sans se soucier d'algorithmique.
Traiter 00E0 et 00EE avant 0NNN, et utiliser le masque 0xF000 pour 0NNN n'est pas une optimisation, mais une correction, avec TON ALGORITHME.
Et l'initialisation condensée de tableaux est possible dans tous les langages de programmation.
Hors ligne BestCoder # Posté le 21/11/2011 à 19:10:29
Best Coder
Avatar

Avis : Très bon

Études : ESIGELEC

Tu parles de correction ?! Elle est où l'erreur à corriger ?

Citation : alexisdm

Et l'initialisation condensée de tableaux est possible dans tous les langages de programmation.

Ça, je viens de l'entendre.


PS: Tu as souligné "algorithme" pour je ne sais quelle raison : Algorithmique existe et c'est bien le mot que je voulais employer.
 
Hors ligne alexisdm # Posté le 22/11/2011 à 00:03:18

J'ai souligné "algorithme" parce que tu crois avoir lu que je voulais que tu changes l'algorithme, alors que je parlais seulement de changer l'ordre des données (et leur structure et leur initialisation pour le second point) :)

Donc, si on oublie le second point, remplacer:
Code : C++
1
2
3
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 */
parCode : C++
1
2
3
4
jp.masque[0]= 0xFFFF; jp.id[1]=0x00E0;          /* 00E0 */
jp.masque[1]= 0xFFFF; jp.id[2]=0x00EE;          /* 00EE */
// On traite 0NNN ici après avoir éliminer les cas qui auraient pu être absorbés
jp.masque[2]= 0xF000; jp.id[0]=0x0000;          /* 0NNN */
et changer également, l'ordre dans le switch/case.

Voir tous les commentaires