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