tu sais que je t'aime bien mon petit coeur de CPU ;) ?

Sous ce titre, que je ne pouvais pas faire en cette semaine de Saint Valentin, se cache en fait la partie la plus sensible de notre émulateur. Nous allons en effet nous attaquer au coeur de notre émulateur : le processeur.

Préliminaires avant de crier (pas de fausses allusions ...)

Bien, nous allons donc parler de la représentation hexadécimale et BCD, car cela sera important de comprendre ces notions dans la suite de ce tutoriel.
Un nombre hexadécimal est en fait un nombre codé en base 16, on le représente souvent avec un h à la fin, h pour hexadécimal, vous suivez ;).
En langage C, on peut directement écrire des nombres hexadécimaux grâce au suffixe 0x devant le nombre
Ainsi, le petit programme suivant
char monNbre;
monNombre = 0x10;

équivaut à mettre le nombre hexadécimal 10 dans monNombre.

Bon ok, mais combien cela fait alors me direz vous ? Je vous rappelle déjà que l’on compte en base 10 (décimal = 10) donc de 0 à 9 puis on ajoute 1 devant si on dépasse 9 et on repart à 0, etc … ainsi 0,1,.., 9,10,11,….19,20.
En base 16, on compte donc jusqu’à 16 pour les unités et comme on ne possède pas d’équivalent dés que l’on dépasse 9, on met ensuite A..F, ainsi on aura 0,1,…9,A,B,C,D,E,F,10,11,..19,1A,…1F,20, ok ?
Ainsi, 10h équivaut à 1*16+0 soit 16 et 1Ch équivaut à 1*16+C (soit 12) donc 16+12 soit 28, vous voyez ?
Pour le codage BCD, nommé Binaire Codé Décimal, les nombres sont représentés en chiffres décimaux et codés sur 4 bits. Pour coder un nombre tel 127 il suffit de coder chacun des chiffres 1, 2 et 7 ce qui donne 0001, 0010, 0111.
Cela revient donc à stocker 01h dans le premier octet, 27h dans le second et ainsi de suite ... Il va de soit que cela prend beaucoup de place si le nombre devient grand ...
Revenons maintenant à nos moutons …

Et il est comment ce processeur alors ?

Le chip 8 est en fait un langage interprété contenu dans les ordinateurs de la fin des années 1970 comme le TelMac 1800 ou le Cosmac VIP. Il est basé sur un processeur CDP-1802.

Comme indiqué dans notre précédente leçon, voici ses spécifications.
Le Chip8 fonctionne avec 4K de mémoire (donc, de 000h à FFFh) et l'interpréteur, dans la machine réelle, est stocké dans le début de la mémoire de 000h à 1FFh. Nos programmes commencent donc à l'adresse 200h.
La résolution graphique du Chip 8 est de 64 pixels en horizontal par 32 en vertical (le SuperChip poussait la résolution à 128 x 64, un exploit ;-) !).
Le Chip 8 possède aussi un "buzzer", en guise de possibilité sonore (je vous rappelle que l'on ne faisait guère mieux sur les micros ordinateurs comme le Sinclair Zx Spectrum ou le Thomson MO5, près de 10 ans plus tard ...).

Les registres de données

Ils sont au nombre de 16, tous en 8 bits et sont nommés V0..VF (on compte en Hexadécimal). VF est utilisé comme Carry (donc comme indicateur permettant de connaître les états des opérations arithmétiques) mais ce dernier sert aussi de détecteur de collision lors de l'affichage des sprites pour le SuperChip.

Le registre d'adresse

Il n'y en a qu'un seul nommé I, de 16 bits. Comme la mémoire ne fait que 4 Ko, seuls les 12 bits de poids faibles sont utilisés. Les 4 bits restants (car 12+4 = 16 bits :-D) sont utilisés par l'instruction LOADFONT que nous verrons plus tard, les caractères étant stockés à l'adresse 8110h.

Les timers

Ils sont au nombre de 2. Un est utilisé pour gérer les délais, et l'autre pour gérer le son. Tous les 2 sont de 8 bits et se décrémentent environ 60 fois par seconde. Le haut-parleur est en fonctionnement tant que le timer gérant le son n'est pas à 0. Le timer de délais est en général utilisé pour réaliser des temporisations.

La pile

Elle contient 16 emplacements, pas plus ! Ce qui nous permet donc de faire 16 appels successifs à des sous programmes.
Comment cela me direz vous ? Et bien, lorsqu'un programme appelle un sous programme, il doit savoir de quel endroit il est parti pour pouvoir revenir ensuite, à la fin du sous programme, c'est pour cela que l'adresse de laquelle il part est stocké dans la pile. Ainsi, notre pile ne possédant que 16 emplacements, nous ne pouvons faire que A6 "sauts" consécutifs, sinon, on écrase forcément une valeur dans notre pile :).
Il semblerait, par contre, que la pile ne soit disponible que sur le SuperChip, la documentation du Chip 8 ne faisant aucune référence à cette dernière.
Voilà tout le contenu de notre processeur, il va falloir faire avec cela ;).

Mais il connait des instructions au moins, il fait quelque chose non ?

Oui, et je vous rappelle d’abord comment cela est géré. En fait, chaque instruction est connue par un numéro, que l’on nomme opcode et suivant ces opcodes, on réalise donc des traitements. Souvent, on fait correspondre à un opcode une instruction dans le langage assembleur « en clair ». Il est en effet plus simple de retenir l’instruction cls que l’opcode 00E0h, quoi que cela peut devenir un jeu chez certains en vous traitant de C3 (en assembleur 8086), ce qui correspond à l’instruction assembleur NOP, soit Non Operation, bref, ne fait rien, soit encore bon à rien … ;). On a aussi vu des virus remplir la mémoire de nos PC, dans les débuts, avec les opcodes ADh et DEh, qui , mis bout à bout, donné DEAD, soit Mort :O !

Voici l'ensemble de ces possibilités résumées dans ce "petit" tableau. Petite précision quand même, dans ce tableau x est un nombre en représentation hexadécimale, r et y sont utilisés pour parler du registre vr (donc si r=3, on aura v3 et si y = 5, v5).
 
Opcode Instruction assembleur Description Notes
       
0xxx -/- Appel des programmes de l'interpréteur Non émulé
00Cx Scdown x Scroll l’écran vers le bas de n lignes Super chip seulement
00E0 Cls Efface l’écran  
00EE Rts Fin de sous programme (donc retour au programme appelant)  
00FB Scright Scroll l’écran de 4 pixels vers la droite Super chip seulement
00FC Scleft Scroll l’écran de 4 pixels vers la gauche Super Chip seulement
00FD quit Quitte l'interpréteur Super Chip seulement
00FE Low Sort du mode graphique étendu Super chip seulement
00FF High Passe en mode graphique étendu (128x64) Super chip seulement
1xxx Jump xxx Saute à l’adresse xxx  
2xxx Jsr xxx Appel le sout programme à l’adresse xxx 16 appels maximum (cf la pile plus haut)
3rxx Skeq vr,xx Ne fait pas l’instruction suivante si registre vr = xx  
4rxx Skne vr,xx Ne fait pas l’instruction suivante si registre vr <>xx  
5ry0 Skeq vr,vy Ne fait pas l’instruction suivante si registre vr = vy  
6rxx Mov vr,xx Met xx dans vr  
7rxx Add vr,xx Ajoute xx à vr Pas de carry générée si on dépasse le maximum de 8 bits autorisés
8ry0 Mov vr,vy Met le registre vy dans vr  
8ry1 Or vr,vy Fait l’opération OU avec vr et vy dans vr  
8ry2 And vr,vy Fait l’opération ET avec vr et vy dans vr  
8ry3 Xor vr,vy Fait l’opération OU EXCLUSIF avec vr et vy dans vr  
8ry4 Add vr,vy Ajoute vy dans vr Carry dans vf
8ry5 Sub vr,vy Fait l’opération vr = vr –vy Carry dans vf si résultat <0
8r06 Shr vr Décalage arithmétique à droite de vr Equivaut donc à vr = vr /2.
Le bit 0 va dans vf
8ry7 Rsb vr,vy Fait l’opération vr = vy – vr Carry dans vf si résultat < 0
8r0E Shl vr Décalage arithmétique à gauche de vr

Equivaut donc à vr = vr * 2.
Le bit 7 va dans vf

9ry0 Skne vr,vy Ne fait pas l’instruction suivante si registre vr <>vy  
Axxx Mvi xxx Met xxx dans le registre I  
Bxxx Jmi xxx Va à l’adresse v0 + xxx  
Crxx Rand vr,xx Met un nombre aléatoire entre 0 et xx dans vr  
Drys Sprite vr,vy,s Affiche le sprite en position vr,vy , de hauteur s Les sprites sont stockés en mémoire à l'adresse contenue dans I. Ils font au maximum 8 pixels de long. Si ils superposent un autre sprite, vf est positionné à 1. Tous les affichages sont de type xor (un pixel affiché est enlevé si un autre pixel vient dessus).
Dry0 Xsprite vr,vy Affiche un sprite étendu en vr,vy Comme ci dessus mais le sprite est en 16x16, super chip seulement
Ek9E Skpr k Ne fait pas l’instruction suivant sur la touche k (registre rk) est enfoncée K est un nombre  
EkA1 Skup k Ne fait pas l’instruction suivant sur la touche k (registre rk) n’est pas enfoncée  
Fr07 Gdelay vr Met le contenu du timer de délais dans vr  
Fr0A Key vr Attend qu’une touche soit appuyée et met cette touche dans vr  
Fr15 Sdelay vr Met vr dans le timer de delais  
Fr18 Ssound vr Met le contenu du timer de son dans vr  
Fr1E Adi vr Ajoute le registre vr au registre I  
Fr29 Font vr Pointe I vers le sprite de caractère hexadécimal contenu dans vr Les sprites sont de 5 pixels de haut
Fr30 Xfont vr Pointe I vers le sprite de caractère hexadécimal contenu dans vr Les sprites sont de 10 pixels de haut, super chip seulement
Fr33 Bcd vr Stock la représentation BCD (Binaire codé décimal) du registre vr à l’adresse I, I+1 et I+2 Ne change pas I
Fr55 Str v0-vr Stock les registres v0-vr à l’adresse pointée par I I est incrémenté pour arriver à l’adresse suivante, donc à r+1
Fx65 Ldr v0-vr Stock dans les registres v0-vr le contenu pointé par I I est incrémenté pour arriver à l’adresse suivante, donc à r+1

Il est à noter que toutes les instructions commençant par 0xxx ne sont pas émulées car l'interpréteur est émulé. Seules les instructions 00E0, 00EE sont utilisées car permettent, respectivement d'effacer l'écran et de quitter un sous programme.

Ok pour le bla bla, on passe à la réalisation maintenant ?

Oui, il est grand temps de commencer :). Nous allons utiliser aujourd'hui 2 fichiers : chip8.c et son pendant chip8.h. Ces fichiers vont contenir tout le nécessaire pour le fonctionnement de notre émulateur.
Tout d'abord, nous allons déclarer la structure qui va contenir les registres de notre CPU. Elle contient tous les registres présentés ci - dessus. On pourrait déclarer des variables directement, mais cela fait plus propre de passer par une structure, pour "regrouper" tout ce qui concerne notre CPU.

Ensuite, nous allons déclarer une variable du type de cette structure afin de pouvoir l'utiliser. Nous déclarons aussi tout ce qui transite autours de notre CPU, comme les buffers des touches, le buffer graphique ou encore la mémoire adressable de notre émulateur.

Nous continuons alors par une fonction importante, elle fait une initialisation de la machine émulée (on nomme cela le RESET).

La seconde fonction importante permet la lecture des opcodes. DES opcodes allez vous me dire ? effectivement, nous avons vu dans le tableau des instructions que les opcodes étaient de la forme wxyz, ils sont donc stockés sur 2 octets. Pour nous simplifier le travail ensuite, on allons en fait les mettre dans 4 variable, chacune contenant les 4 morceaux de l'opcode (w x y z).
Cet exemple permet de voir l'utilisation de la fonction read_mem qui permet de lire une case de la mémoire. L'opcode correspondant en fait à l'instruction exécutée par notre CPU, nous devons donc lire l'adresse mémoire qui se dernier est en train de pointer.
Enfin, le >> 4 permet de décaler de 4 bits vers la droite la valeur, ainsi nous aurons bien dans notre premier opcode une valeur entre 0 et 15 correspondant au w de mon exemple ci-dessus.

Et enfin, nous attaquons les fonctions émulées du processeur. Nous n'allons pas parler des fonctions du Super Chip pour l'instant, uniquement les fonctions du Chip8. Nous ne détaillerons pas toutes les fonctions ici, elles se ressemblent et se bornent à réaliser l'action demandée, conformément au tableau présenté ci-dessus.

La première émule l'instruction d'effacement de l'écran, on remet donc à 0 toutes les cases de notre tableau contenant les graphismes.
La seconde permet de sortir d'un sous programme, on remet donc la pile au bon endroit et ensuite le PC reprend sa valeur avant l'appel du sous programme.
La troisième permet de faire un JUMP à un autre endroit de la mémoire, donc le PC prend la valeur de tous les opcodes :), simple non ?.
Voici maintenant la différence avec l'instruction JSR qui permet d'appeler un sous programme, vous voyez que dans ce cas là, la pile sauvegarde la valeur de l'endroit d'où on part.

Nous terminerons cette leçon par la fonction d'exécution, qui permet donc de mettre en relation tout ce que nous venons de voir ci-dessus. Elle lit donc l'opcode et exécute la fonction voulue suivant cet opcode.
La première partie contient donc la boucle suivant le temps décidé d'exécution, puis va lire l'instruction pointée actuellement par le PC.

La seconde partie est en fait un ENORME test contenant tous les cas possibles. L'avantage du case est que cela permet de bien comprendre tous les cas possibles. Une des manières d'améliorer la rapidité sera de faire un tableau d'appel de fonctions, mais cela n'est pas la peine pour le Chip8, la rapidité sera au rendez vous.

Petite modification de notre programme principal pour intégrer l'initialisation du processeur et l'exécution des cycles d'instructions.

Voilà, c'est tout pour cette fois ci, la prochaine fois, nous attaquerons la gestion des entrées / sorties (le graphisme, le clavier et le son) et nous pourrons enfin avoir un émulateur qui fonctionne !!!
En effet, nous mettrons en place la gestion des touches et la gestion de l'écran supérieur de la DS pour afficher les graphismes du Chip8. Il est à noter que nous enlèverons le mode DEBUG de notre émulateur (vous savez, tous les #ifdef DEBUG dans le code ;-) ) pour afficher le clavier sur l'écran du bas.

N'hésitez pas à tester le programme actuel, j'ai rajouté des données dans la mémoire pour vous montrer l'exécution des instructions.

Il suffit de modifier les valeurs de la mémoire pour faire faire autre chose à notre programme.
Hal8000 Jour2  (138 fois)16 Feb 2007 | size: 426 kB
Version qui émule le coeur 1802