kuji@portfolio:~/projets$ cat chip8.md

Émulateur CHIP-8 en C

Développement from scratch d'un émulateur de console rétro, basé sur la manipulation de bits et la librairie SDL2.

C Bas niveau SDL2

Pourquoi ce projet

Je voulais un projet un peu plus concret avec un vrai rendu visuel. Faire un émulateur from scratch en C, c'était le bon prétexte pour m'entraîner à fond sur la manipulation bit à bit tout en comprenant vraiment comment un processeur fonctionne.

Source

Lien vers le repo GitHub

Architecture et Fonctionnement

Le CHIP-8 est un langage interprété conçu dans les années 70. Mon émulateur recrée virtuellement les composants physiques de la machine de l'époque :

  1. Le CPU et la Mémoire : L'émulateur possède 4096 octets de mémoire (RAM), 16 registres de 8 bits (V0 à VF), une pile d'appels (Stack) de 16 niveaux et un registre d'adresse (I).
  2. Le cycle "Fetch-Decode-Execute" : Dans la boucle principale, l'émulateur lit une instruction de 16 bits en mémoire, sépare ses différentes parties grâce à des masques binaires (ex: opcode & 0xF000), et exécute l'action correspondante (saut, addition, dessin...).
  3. Affichage et Rendu : L'écran d'origine fait 64x32 pixels monochromes. Les sprites sont dessinés grâce à une opération logique XOR (OU exclusif). J'ai utilisé SDL2 pour générer la fenêtre et mettre les pixels à l'échelle pour les écrans modernes.
  4. Audio et Timers : Le processeur tourne très vite, mais les timers (délai et son) doivent être rafraîchis exactement 60 fois par seconde. J'ai configuré la boucle de rendu pour limiter la vitesse (60 FPS) et utilisé SDL_Audio pour générer dynamiquement une onde carrée (le fameux "bip" rétro).

Utilisation

Le projet se compile avec GCC en liant la bibliothèque SDL2 :

$ gcc chip8.c -o chip8 -lSDL2
# Lancement avec une ROM au choix (ex: Pong)
$ ./chip8 Pong.ch8

Extrait du code source

Voici un aperçu de la boucle principale du CPU. On y voit la phase de décodage avec les masques binaires, suivie de l'instruction 0xD qui est le cœur graphique du CHIP-8 : elle lit un sprite en mémoire bit par bit et l'affiche à l'écran en utilisant un OU exclusif (XOR).

chip8.c (Extrait)
            // Lecture de l'instruction sur 16 bits
            uint16_t opcode = (chip.memory[chip.pc] << 8) | chip.memory[chip.pc + 1];
            chip.pc += 2;
            
            // Phase de décodage (extraction via masques binaires)
            uint16_t type = (opcode & 0xF000) >> 12;
            uint16_t X    = (opcode & 0x0F00) >> 8; 
            uint16_t Y    = (opcode & 0x00F0) >> 4; 
            uint16_t N    = opcode & 0x000F;        
            uint16_t NN   = opcode & 0x00FF;        
            uint16_t NNN  = opcode & 0x0FFF;        

            // Phase d'exécution
            switch (type) {
                // ... (autres instructions)

                case 0xD: {
                    // DXYN : Dessine un sprite de N lignes aux coordonnées (VX, VY)
                    chip.V[0xF] = 0; // Réinitialise le flag de collision
                    
                    for (int i = 0; i < N; i++) {
                        uint8_t sprite_byte = chip.memory[chip.I + i];
                        
                        for (int j = 0; j < 8; j++) {
                            uint8_t x = (chip.V[X] + j) % 64;
                            uint8_t y = (chip.V[Y] + i) % 32;
                            
                            // Isole le bit courant du sprite
                            uint8_t sprite_pixel = sprite_byte & (0x80 >> j);
                            
                            if (sprite_pixel != 0) {
                                // Si un pixel de l'écran est déjà allumé, il y a collision
                                if (chip.screen[y][x] == 1) {
                                    chip.V[0xF] = 1;
                                }
                                // XOR pour allumer/éteindre le pixel
                                chip.screen[y][x] ^= 1;
                            }
                        }
                    }
                    break;
                }
                
                // ... (suite du switch)
            }