Premier pas dans l’émulation : chip8 en c

William Benakli – Bilal Seddiki – Alban Le Jeune

Histoire du chip8

Dans le cadre d’un projet d’étude en trinôme, nous avons été amené à développer un émulateur. On ne savait pas exactement vers quoi se tourner mais après quelques recherches nous sommes tombés sur CHIP-8. Le CHIP-8 est un langage de programmation et une machine virtuelle développée dans les années 1970. Créé par Joseph Weisbecker pour le COSMAC VIP, un micro-ordinateur, le CHIP-8 a été conçu pour simplifier le développement de jeux vidéo. Il comportait un ensemble d’instructions relativement simple et était portable sur différents systèmes. Bien que le COSMAC VIP n’ait jamais été largement adopté par les utilisateur. Ce n’est donc pas un système à proprement parlé. On parle donc plutôt d’interpréteur ici pour être exact, mais le terme d’émulateur n’est pas complètement erroné dans ce cas. En effet, tenter de le reproduire constitue généralement le premier pas pour quiconque désire explorer ce domaine.

Les principales sections du Chip8

La structure initiale

Pour la conception de départ du chip8, nous l’avons représenté par une structure de donnée Chip8 qui contient les principales sections et état d’un Chip8.

Représentation en code de la structure de donnée : Chip8

Dans la struct Chip8 il y’a de nombreux attributs, nous allons nous détaillé l’utilité des principaux attributs de la structure.

program_counter avec index_register

La stack

La stack dans CHIP-8 est une pile utilisée pour gérer les appels de sous-routines. Lorsqu’une instruction de saut à une sous-routine est exécutée (2NNN), l’adresse de retour (adresse de l’instruction suivante) est sauvegardée dans la stack. Ensuite, le compteur de programme (PC) est mis à jour pour pointer vers l’adresse de la sous-routine. Quand une instruction de retour de sous-routine (00EE) est exécutée, l’adresse sauvegardée est extraite de la stack, et le PC est mis à jour pour continuer l’exécution à cette adresse. La stack de CHIP-8 a une taille de 16 niveaux, ce qui est généralement suffisant pour la plupart des programmes CHIP-8.

L’affichage simple

L’affichage dans CHIP-8 est très basique et utilise une grille de pixels de 64×32. Chaque pixel est soit allumé (1) soit éteint (0). Les instructions de dessin (DXYN) permettent de dessiner des sprites, qui sont des bitmaps de 8 bits de largeur et de N bits de hauteur, sur l’écran. Les sprites sont XORés avec l’écran, permettant ainsi de créer des effets de collision simples.

L’affichage est mis à jour à intervalles réguliers grâce à un timer, assurant que les modifications apportées par les instructions de dessin sont reflétées à l’écran de manière cohérente.

La mémoire: uint8_t memory[MEMORY_SIZE];

La mémoire à une taille de 4 ko. Le registre d’index, le compteur de programme et les entrées de la pile ont tous une longueur réelle de 16 bits. Toute la mémoire est RAM et doit être considérée comme inscriptible. Les programmes CHIP-8 que vous trouvez en ligne sous forme de fichiers binaires sont souvent appelés des “ROMs”, comme les fichiers de jeux pour les émulateurs de jeux vidéo, mais contrairement aux jeux sur cartouches de console, ils n’étaient pas réellement ROM (ce qui signifie “read-only memory”).

Les instructions vont s’exécuter dans la mémoire, et seront placés à une certaine position lors du chargement de la rom. Le compteur de programme va se positionner sur l’instruction courante et l’instruction sera traitée dans la boucle du code. Mais avant nous la transformons en structure de donnée nommé nibble.

Les instructions

Qu’est-ce qu’un nibble ?

Nous avons décidé d’utiliser une structure de donnée nommé nibble pour interpréter les instructions de la rom placées en mémoire.

Représentation en code de la structure de donnée : Nibble

Les instructions sont décodés en structure nibble. Cela simplifie grandement l’accès aux différentes instructions. Il est donc plus simple d’accéder aux valeurs pour chaque instruction. Nous avons fait cela à l’aide de notre fonction décode, qui décale les bits contenu dans le opcode (l’instruction lu) et on attribut ce décalage de bit à l’objet nibble.

Fonction decode() présente dans chip8.c

Voici un exemple pour cette instruction “1NNN”. Ici 1 NNN, le opcode = 1 et le champs .nnn réprésente directement notre instruction. Donc il est plus facile de simplement décoder ça en object nibble et son utilisation nous simplifiera tout le travail par la suite.

Liste des instructions

Il existe une liste non exhaustives d’instructions. Disponible ici, sur le Wikipédia de CHIP8. Certaines instructions sont plus utilisés que d’autres. Elle peuvent se répertorier en groupe d’instructions.

Groupe d’instructions :

  • Contrôle de flux
  • Opérations arithmétiques et logiques
  • Opérations sur les registres et la mémoire
  • Entrée et sortie

La boucle principale

La boucle principale est au centre du programme chip8.c son principe est que chaque instruction lu dans la rom soit traité. L’idée peut se représenter avec un schéma :

Chaque instruction lu sera transmise à la boucle, et en fonction de l’instruction. Des actions seront effectués sur le Chip8.

Entrons plus en détail sur la conception de la boucle.

Dans un premier temps, lorsque le programme se lance. Le fichier ROM se charge dans la mémoire, ou les instructions sont chargés (vu plutôt). Pour cela nous avons conçu la fonction load_rom().

Capture d’écran de la fonction load_rom. Cette fonction permet de charger la rom

Avec cette fonction nous avons chargé en mémoire dans la structure chip8, toutes les instructions présente dans le fichier rom.

Boucle d’exécution

  1. Tant que le programme tourne, la boucle principale :
    • Fetch : Récupère l’instruction pointée par le compteur de programme (PC).
    • Decode : Décode l’instruction récupérée pour déterminer de quelle opération il s’agit.
    • Execute : Exécute l’instruction décodée en effectuant les opérations nécessaires.
    • Timers : Met à jour les timers.
    • PC Update : Met à jour le PC pour pointer vers la prochaine instruction.

Le keypad, l’interface homme – chip8

Nous avons à présent un jeu qui tourne correctement et avec une rendu visuelle. Il faut donc avoir des interactions entre le chip8 et l’utilisateur. Pour cela il faut faire ce qu’on appelle du mapping de touche. En effet chaque action effectué sur le clavier est intercepté lorsque le chip8 est exécuté. Le keypad de CHIP-8 utilise un mapping de touches spécifique. Les touches de 0 à F sur le clavier sont mappées de manière à correspondre aux touches hexadécimales du CHIP-8. Voici un exemple de mapping typique :

1 2 3 C    ->   1 2 3 4
4 5 6 D -> Q W E R
7 8 9 E -> A S D F
A 0 B F -> Z X C V

Lorsque l’utilisateur appuie sur une touche, l’émulateur met à jour l’état du keypad dans la mémoire. Les instructions qui nécessitent une interaction utilisateur, comme attendre une pression de touche (FX0A), utilisent cet état pour continuer l’exécution.

SDL

Depuis le début on reste flou quant à l’affichage et au rendu du système. Il s’avère que nous avons utilisé la libraire SDL (Simple DirectMedia Layer), une bibliothèque multi-plateforme qui nous permet de gérer des opérations de bas niveau telles que l’affichage graphique.

Pour gérer l’affichage des pixels, nous avons utilisé SDL pour créer une fenêtre et un rendu. Chaque pixel du CHIP-8 (64×32) est représenté graphiquement en utilisant SDL, ce qui permet un rendu visuel clair et précis.

Initialisation de SDL :

Nous avons initialisé SDL avec SDL_Init(SDL_INIT_VIDEO) pour préparer la bibliothèque à l’utilisation vidéo.

Création de la fenêtre et du rendu :

Une fenêtre et un rendu SDL ont été créés pour afficher les graphiques du CHIP-8. Cela nous permet de dessiner les pixels de l’émulateur sur l’écran de manière fluide.

On obtient un rendu graphique plus détaillé et à l’échelle.

Désassembleur, pour y voir plus clair

Du binaire à l’assembleur

Lorsqu’on observe les roms trouvées sur internet. En ouvrant la rom tetris.rom par exemple. On remarque qu’on ne comprend pas les instructions de la rom, on obtient du texte illisible. Il s’agit d’un fichier binaire.

Capture d’écran du fichier tetris.rom

Après l’exécution de notre programme désassembleur sur la rom on obtient un autre fichier nommé : tetris.asm, avec des instructions lisibles.

Capture d’écran du fichier tetris.asm

Les instructions dans la rom

Les instructions sont écrit comme en assembleur et dans un certain ordre. Ce qui facilite la compréhension. Allons plus loin dans l’explication.

On remarque clairement 2 séparation dans ces instructions:

Capture d’écran des instructions après l’exécution du désassembleur

En rouge c’est l’index où se trouve l’instruction dans le fichier. Il commence à la position 200. En bleu c’est l’instruction avec (ou sans) ses arguments. L’instruction est limité à 16 octets.

Les cas d’erreurs

Il peut arriver à certain moment que les instructions ne soient pas à proprement traduite par le désassembleur car non prise en compte, dès lors on obtient une ligne d’erreur “Unknown opcode : code”.

Nous venons de voir ce que le désassembleur produit sur les fichiers roms. Regardons ensemble son fonctionnement.

Le fonctionnement de notre désassembleur

Le fonctionnement est très proche de la boucle principale du chip8. On retrouve le même schéma d’exécution.

Chaque instruction est lue et placée dans un objet nibble avec les fonctions fetch() et decode(). (même principe que la boucle principale)

Une fois l’objet nibble formé, on appelle la fonction log_op avec la structure nibble en paramètre.

Dans la fonction log_op, on lit la structure nibble. On la détermine et en fonction des valeurs présentent dans le nibble on écrit la traduction de l’instruction avec son index dans la rom dans le buffer.

Capture d’écran de la fonction log_op qui écrit dans le buffer la valeur comprise dans la structure de donnée nibble.

Enfin, après la lecture de toutes les instructions, on écrit les valeurs stocké dans le buffer sur un fichier. On obtient un fichier avec des instructions lisibles.

Conclusion sur Chip8

Le Chip8 est une réelle porte d’entrée au monde de l’émulation, même s’il ne s’agit pas l’un d’un vrai émulateur à proprement parlé, actuellement vous avez de bonnes pistes sur comment fonctionne un émulateur dans son ensemble. Cela va vous permettre, qui sait, d’aller plus loin dans la conception d’émulateur.

Pour aller plus loin

Maintenant, vous pouvez aller plus loin pour vous amuser. Il est possible, par exemple, de rendre votre chip8 compatible avec Retro Arch, comme vu dans l’article de mon binôme Bilal Seddiki, et ainsi pouvoir le déployer sur plusieurs machines: téléphone, borne d’arcade etc. Ou encore faire la version couleur du Chip8. Toutes ces idées sont à votre portées.

Nos sources

http://www.mac-emu.net/dossiers/un-peu-d-histoire/article/qu-est-ce-que-l-emulation
https://www.grospixels.com/site/emustory.php
https://www.giantbomb.com/emulation/3015-1587/
https://stackoverflow.com/questions/1584617/simulator-or-emulator-what-is-the-difference
https://www.tomshardware.fr/lemulation-comment-pourquoi/
https://softwarerecs.stackexchange.com/questions/56641/software-to-fully-simulate-machine-language-instructions-8051
https://www.dz-techs.com/fr/virtualization-vs-emulation/
http://www.ordinateur.cc/syst%C3%A8mes/Comp%C3%A9tences-informatiques-de-base/200886.html
https://fr.wikipedia.org/wiki/CHIP-8
https://tobiasvl.github.io/blog/write-a-chip-8-emulator/#make-your-own-chip-8-game

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *

Ce site utilise Akismet pour réduire les indésirables. En savoir plus sur comment les données de vos commentaires sont utilisées.