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.
#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
#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
}
#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
#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
}
}
}
}
}
#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.
Ç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 |