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.
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 GitHubArchitecture 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 :
- 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).
- 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...). - 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.
- 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_Audiopour 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).
// 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)
}