Nous allons commencer par récupérer une citation dans la description de la Chip 8, que nous nous contenterons de traduire en langage machine.
Cette partie concernera le
CPU de la Chip 8. Le CPU est l'organe central de notre émulateur : c'est le chef d'orchestre.
Are you ready?
La mémoire
CitationLes adresses mémoire de la Chip 8 vont de $200 à $FFF (l'hexadécimal revient), faisant ainsi 3 584 octets. La raison pour laquelle la mémoire commence à partir de $200 est que sur le VIP et Cosmac Telmac 1800, les 512 premiers octets sont réservés pour l'interpréteur. Sur ces machines, les 256 octets les plus élevés ($F00-$FFF sur une machine 4K) ont été réservés pour le rafraîchissement de l'écran, et les 96 octets inférieurs ($EA0-$EFF) ont été réservés pour la pile d'appels, à usage interne, et les variables.
Bien que cette citation soit assez longue, ce qui nous intéresse est : « Les adresses mémoire vont de $200 à $FFF, faisant ainsi 3 584 octets » et « les 512 premiers octets sont réservés ». Je rappelle que
$200 = 512.
On peut déduire de ces deux informations que la Chip 8 a une mémoire de
3 584 + 512 = 4 096 octets (un octet = huit bits, ne l'oubliez jamais). Le reste n'est que culture générale.
Et comme nous allons simuler le fonctionnement de notre machine, le rafraîchissement sera géré par une autre méthode. Il existe des fonctions dédiées pour toutes les bibliothèques graphiques (
update,
repaint,
SDL_Flip, etc). Les 512 premiers octets ne serviront donc à rien
(pour le moment).
Dans mon cas, la variable mémoire prendra la forme d'un tableau de
4 096 octets.
La mémoire est utilisée pour charger les jeux (roms) et pour la gestion des périphériques de la machine.
J'en profite pour vous dire qu'il faut bien prendre en compte la taille spécifiée pour chaque variable. En plus, elles sont toutes non signées. En cas de non-respect de ces indications, votre programme buggera à coup sûr.
Je parle en connaissance de cause.
Donc, à nous les
unsigned dans tous les sens ! Pour ma part, j'utilise SDL, donc à moi les
Uint.
Déclaration de la mémoire :
Uint8 memoire[4096]; // la mémoire est en octets (8 bits), soit un tableau de 4096 Uint8.
Maintenant, pour pointer sur une adresse donnée, il faut une autre variable qui sera initialisée à
$200 = 512 comme nous le dit la description.
Nous la nommerons
pc comme «
program counter ». La variable doit être de 16 bits au minimum car nous devons être en mesure de parcourir tout le tableau mémoire qui va de
0 à 4095.
Les registres
CitationLa Chip 8 comporte 16 registres de 8 bits dont les noms vont de V0 à VF (F = 15, encore l'hexadécimal). Le registre VF est utilisé pour toutes les retenues lors des calculs.
En plus de ces 16 registres, nous avons le registre d'adresse, nommé I, qui est de 16 bits et qui est utilisé avec plusieurs opcodes qui impliquent des opérations de mémoire.
Ici, il n'y a rien de compliqué, nous nous contenterons donc juste de déclarer les variables. Les registres permettent à la Chip 8 − et à tout processeur en général − de manipuler les données. Ils servent en gros d'intermédiaires entre la mémoire et l'unité de calcul, ou l'
UAL (Unité Arithmétique et Logique) pour les intimes. Le processeur gagne en vitesse d'exécution en manipulant les registres au lieu de modifier directement la mémoire.
La pile ou stack
CitationLa pile sert uniquement à stocker des adresses de retour lorsque les sous-programmes sont appelés. Les implémentations modernes doivent normalement avoir au moins 16 niveaux.
Lorsque le programme chargé dans la mémoire s'exécute, il se peut qu'il fasse des sauts d'une adresse mémoire à une autre.
Pour revenir de ces sauts, il faut sauvegarder l'adresse où il se trouvait avant ce saut (
pc) : c'est le rôle de la pile, appelée
stack en anglais. Elle autorise seize niveaux, il nous faudra donc un tableau de seize variables pour stocker les seize dernières valeurs de
pc ; on le nommera
saut.
Et comme pour la mémoire, on aura besoin d'une autre variable afin de parcourir ce tableau. Cette fois-ci, le type
Uint8 fera l'affaire puisqu'on ne parcourt que seize valeurs. Je l'ai nommée
nbrsaut.
Les compteurs
CitationLa Chip 8 est composée deux compteurs. Ils décomptent tous les deux à 60 hertz, jusqu'à ce qu'ils atteignent 0.
Minuterie système : cette minuterie est destinée à la synchronisation des événements de jeux. Sa valeur peut être réglée et lue.
Minuterie sonore : cette minuterie est utilisée pour les effets sonores. Lorsque sa valeur est différente de zéro, un signal sonore est émis. Sa valeur peut être réglée et lue.
La Chip 8 a besoin de deux variables pour se charger de la synchronisation et du son. Nous les appellerons respectivement
compteurJeu et
compteurSon.
Puisqu'elles doivent décompter à 60 hertz, il faut trouver une méthode pour les décrémenter toutes les
1 / 60 = 0,016 = 16 millisecondes. Les
timers restent une bonne solution pour effectuer ce genre d'opération. En SDL, on implémente cette action avec
SDL_Delay.
Toutes les caractéristiques de la Chip 8 seront stockées dans une structure qui représentera le CPU.
Secret (cliquez pour afficher)
Code : C - cpu.h 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 | #ifndef CPU_H
#define CPU_H
#define TAILLEMEMOIRE 4096
#define ADRESSEDEBUT 512
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 « mémoire », 16 au maximum
Uint8 nbrsaut; //stocke le nombre de sauts effectués pour ne pas dépasser 16
Uint8 compteurJeu; //compteur pour la synchronisation
Uint8 compteurSon; //compteur pour le son
Uint16 pc; //pour parcourir le tableau « mémoire »
} CPU;
CPU cpu; //déclaration de notre CPU
void initialiserCpu() ;
void decompter() ;
#endif
|
Code : C - cpu.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 | #include "cpu.h"
void initialiserCpu()
{
//On initialise le tout
Uint16 i=0;
for(i=0;i<TAILLEMEMOIRE;i++) //faisable avec memset, mais je n'aime pas cette fonction ^_^
{
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;
}
void decompter()
{
if(cpu.compteurJeu>0)
cpu.compteurJeu--;
if(cpu.compteurSon>0)
cpu.compteurSon--;
}
|
Maintenant, attaquons le graphique, cela nous permettra de voir rapidement les différents résultats. L'ordre d'implémentation des caractéristiques importe peu, vous pourriez commencer par le graphique ou même l'exécution des instructions si vous le vouliez
(par contre, je ne vous le conseille pas).
Cet ordre nous permettra de faire des tests le plus tôt possible.