Accueil
GameBoy Color faisant tourner la ROM produite dans cet article
Catégorie : Programmation

Introduction à la programmation sur GameBoy

Introduction de l'introduction

Cet article est une courte introduction à la programmation pour GameBoy, la partie technique est volontairement simplifiée pour être compréhensible par le plus grand nombre

Si vous êtes vieux vous avez sans doute déjà joué au GameBoy, inutile de la présenter tellement elle est connue. Aujourd'hui on va s'intéresser à sa partie technique et plus précisément sur la façon dont on réalise un programme pour cette machine. Pour simplifier au maximum on va juste s'intéresser au GameBoy et GameBoy Pocket (Désolé pas de couleur cette fois-ci).

Caractéristiques techniques

La GameBoy n'est rien d'autre qu'un ordinateur (Oui oui c'est fou !) et comme tout ordinateur on y retrouve des éléments communs :

Le processeur : Il s'agit d'un Sharp SM83 (Un hybride entre un Zilog Z80 et un Intel 8080 avec moins de registres et des différences d'opérandes). Processeur 8 bits avec un adressage 16 bits (On va y revenir), il est cadencé à 4.194304 MHz et dispose de 128 octets de mémoire qu'on appelle la HRAM, elle sert principalement pour la pile et les routines DMA, mais on peut s'en servir pour autre chose.

La RAM : On dipose ici de 16 Ko de WRAM dont 8 Ko réellement utilisables séparé en deux banques (WRAM0 et WRAM1), les deux dernières banques sont une copie exacte des deux premières et il n'est pas recommandé d'écrire dedans même si c'est possible (Pour l'anecdote c'est comme ça qu'on peut faire des bugs sur Pokémon).

L'écran : Un petit écran à cristaux liquides (LCD) capable d'afficher 4 nuances de gris (ou de vert) d'une définition de 160x144 points. Sa fréquence de rafraichissement verticale est de 60 Hz.

Le processeur audio : Il dispose de 4 canaux contrôlables indépendamment.

Les entrées et sorties : Une croix directionnelle à 4 positions, 4 boutons (A, B, Start et Select) ainsi qu'un port de communication série.

Comme dit plus tôt le processeur dispose d'un adressage 16 bits ce qui nous donne donc 64 Ko qui se présentent comme ceci :

$0000 - $3FFF La première banque de la cartouche qui fait 16 Ko, elle reste fixe
$4000 - $7FFF La deuxième banque de la cartouche qui fait également 16 Ko, celle-là est capable de bouger afin d'avoir des jeux de plus de 32 Ko
$8000 - $9FFF Les 8 Ko de VRAM (Pour l'affichage des tuiles et des maps)
$A000 - $BFFF Les 8 Ko de SRAM si on en dispose dans la cartouche
$C000 - $CFFF La première banque de 4 Ko de WRAM
$D000 - $DFFF La deuxième banque de 4 Ko de WRAM
$E000 - $FDFF Comme dit précédemment c'est une copie des deux premières banques mémoire (ECHO RAM)
$FE00 - $FE9F La RAM pour l'OAM (les sprites)
$FF00 - $FF7F Les entrées et sorties
$FF80 - $FFFE La HRAM
$FFFF Pour activer les interruptions

Le processeur

Comme vu plus haut le processeur de la GameBoy est un hybride entre un Zilog Z80 et un Intel 8080, il dispose de 8 registres 8 bits identiques à l'Intel 8080 (A (Plus communément appelé accumulateur), B, C, D, E, F, H, L) qui peuvent se combiner pour former des registres 16 bits (AF, BC, DE, HL), on y retrouve également le pointeur de pile (SP) et le pointeur d'instruction (PC).
Ce sont de petits emplacements de stockage temporaire, on peut tous les utiliser sauf F qui sert à indiquer l'état des drapeaux.

En parlant des drapeaux, il en existe 4 :

Il est capable d'aditionner et de soustraire mais pas de multiplier ou diviser (Il faut écrire des routines pour ça).

On dispose d'une pile de type LIFO (Last in, first out)
Pour bien comprendre : faites une pile d'assiettes, la dernière posée sera la première retirée ;)

Pour finir voici le tableau des opérandes que peut gérer le processeur.

L'écran

L'écran LCD a une définition de 160x144 points (20x18 tuiles, une tuile fait 8x8 points) comme vu plus haut et est capable d'afficher 4 nuances de gris, en réalité on a une surface d'affichage bien plus importante (on en a même 2).

On dispose d'un background d'une taille de 256x256 points (32x32 tuiles), celui-ci forme une boucle infinie (Quand on arrive à droite ou en bas il revient automatiquement à gauche ou en haut). Pas de transparence possible avec.

On dispose également d'un window de la même taille qui quand est actif se place par dessus le background mais ne forme pas une boucle infinie contrairement à celui-ci. Également pas de transparence possible avec.

Pour finir on a les sprites (8x8 points ou 8x16 points), on peut en afficher 40 en tout et 10 par lignes, la transparence est possible et active quand le point voulu possède la couleur 0.

Il est possible de stocker 192 tuiles dans la VRAM de la GameBoy. Une tuile fait 16 octets, chaque octet représente 4 points d'une tuile.

Les liens utiles et les outils

Avant de rentrer dans le vif il est important pour vous de connaître quelques ressources utiles pour approfondir ainsi que les outils qu'on va pouvoir utiliser.

Le manuel officiel de programmation pour la GameBoy Très certainement subtilisé à Nintendo
Les pandocs Possiblement le document le plus important pour programmer pour la GameBoy, tout ce que vous avez besoin de savoir est dedans !
RGBDS L'assembleur qu'on va utiliser dans cet article (Composé d'un assembleur, linker et d'un outil pour corriger l'en-tête des ROM)
GameBoy Tile Designer Un programme écrit en Delphi datant de 1997 pour Windows (Fonctionne parfaitement avec Wine). Très utile pour dessiner des tuiles sans se casser la tête
BGB L'un des meilleurs émulateurs GameBoy pour Windows (Fonctionne également très bien avec Wine), il a l'avantage de disposer d'un très bon débugger (Inspiré de NO$GBM)
SameBoy Également un très bon émulateur compatible *NIX mais disposant d'un débugger moins complet et uniquement disponible en mode texte

Le code Les larmes

Pourquoi choisir d'écrire de l'assembleur alors qu'il existe GBDK afin d'écrire du C pour la GameBoy ? Parce qu'avec l'assembleur on a le contrôle sur absolument tout et cela nous permet de comprendre comment fonctionne un ordinateur, au début ça peut faire peur mais vous allez voir ce n'est pas bien compliqué.

Maintenant que c'est dit, on peut commencer la partie délicate, l'objectif ne sera non pas de développer "Léa Passion Assembleur GBZ80" mais d'afficher un très bel "Hello World" à l'écran (Vous allez voir ce n'est pas aussi évident que ça en a l'air). Assurez vous d'avoir installé RGBDS correctement et d'avoir GNU Make sur votre système car on va en avoir besoin !

Afin de se faciliter la vie on va utiliser le Makefile suivant (Pour comprendre ces lignes le manuel des outils de RGBDS est là pour vous, mais concrètement on va faire une ROM de 32 Ko sans SRAM ni compatibilité pour GBC).

ASM := rgbasm
LINKER := rgblink
FIX := rgbfix

ROM_NAME := hello_world

all:
    $(ASM) -o $(ROM_NAME).obj main.asm
    $(LINKER) -p 0x0 -m $(ROM_NAME).map -n $(ROM_NAME).sym -o $(ROM_NAME).gb $(ROM_NAME).obj
    $(FIX) -s -v -j -l 0x33 -r 0 -p 0 -t "$(ROM_NAME)" $(ROM_NAME).gb

Ensuite on va faire un fichier "main.asm" qui va contenir le code suivant.

SECTION "Interruptions", ROM0[$40]

SECTION "VBlank", ROM0[$40]
    reti

SECTION "LCD STAT", ROM0[$48]
    reti

SECTION "Timer", ROM0[$50]
    reti

SECTION "Serial", ROM0[$58]
    reti

SECTION "Joypad", ROM0[$60]
    reti

SECTION "Point d'entrée", ROM0[$100]
    nop
    jp main ; $150

SECTION "En-tête", ROM0[$104]

REPT $150 - $104
    DB 0 ; Remplis l'en-tête de $00
ENDR

SECTION "Variables HRAM", HRAM

vblank_flag EQU $FF80

SECTION "Main", ROM0[$150]
main:
    nop
    jr main

Pas besoin de s'embêter à remplir l'en-tête à la main, rgbfix fait tout le travail pour nous.
Si vous assemblez ce code et que vous le lancez dans un émulateur vous allez vite vous rendre compte qu'il ne se passe pas grand chose car le logo "Nintendo" reste affiché à l'écran mais, en réalité, le processeur est en train de traiter la boucle qu'on a écrite.

jp main Saute au label "main:" (Permet d'éviter à la GameBoy de lire toute la merde de l'en-tête et de sauter directement à $150)
nop Un opérande pour signifier au processeur de ne rien faire
jr main Un autre opérande pour effectuer un saut (Prend un octet de moins que jp mais est plus lent, capable de sauter uniquement entre 128 octets avant ou après)

Pour changer ce que l'écran affiche, on doit écrire dans la VRAM. Il est possible d'écrire dedans uniquement quand l'écran est éteint ou pendant une interruption V-Blank, pour notre exemple on va simplement couper l'écran. Comme écrit plus haut, pour l'éteindre, il faut attendre d'être dans la période V-Blank (Cette période se situe entre la ligne 144 et la ligne 154, soit une courte durée de 1.09 ms).

Nintendo recommande fortement de ne pas éteindre l'écran hors de la période V-Blank, cela peut sur le long terme l'endommager

On va reprendre notre fichier main.asm et s'intéresser à la section VBlank, cette partie s'active lors d'une interruption V-Blank (Si vous avez suivi elle s'active donc environ 60 fois par seconde).

Si vous avez l'œil vous avez pu remarquer une variable vblank_flag stockée dans la HRAM, elle va nous servir à savoir si la période V-Blank a bien eu lieu.

SECTION "VBlank", ROM0[$40]
    call vblank
    reti

Avec CALL on va appeler la fonction vblank (Qui sera écrite plus tard), RETI indique que la fonction est terminée et donc que le processeur doit revenir à la fonction d'avant (RETI est une combinaison de RET et EI pour automatiquement réactiver les interruptions à la fin de la fonction, qui se désactivent après la fin de l'une d'entre elles).

Juste en dessous de notre fonction main on va pouvoir écrire la fonction vblank (elle est un peu longue mais facilement compréhensible).

vblank:
    push af
    push bc
    push de
    push hl

    ; V-Blank flag = $FF80
    ld a, 1
    ldh [vblank_flag], a
    
    pop hl
    pop de
    pop bc
    pop af
    ret

On va pousser tous les registres dans la pile avec PUSH (Étant donné que l'interruption s'active énormément de fois ça serait dommage de perdre nos données) puis on va écrire 1 dans l'accumulateur pour ensuite passer notre variable vblank_flag à 1 (On ne peut pas écrire directement dedans, il est obligatoire de passer par l'accumulateur pour cela), LDH est un opérande spécifique au processeur de la GameBoy, c'est un raccourci de LD $FF. Avec POP on sort toutes les données des registres de la pile pour les réinsérer dans les registres correspondants et comme vu plus haut, RET permet de retourner à la fonction précédente (Trop simple !).

Ok c'est très bien on a fait notre interruption V-Blank mais il nous manque encore la fonction pour attendre cette fameuse période. On va justement pouvoir l'ajouter juste en dessous.

wait_vblank:
    ld hl, vblank_flag
    xor a
.wait
    halt
    cp [hl]
    jr z, .wait
    ld [hl], a
    ret

On envoie le contenu de notre variable vblank_flag dans le registre HL, xor a effectue un OU exclusif sur lui-même et forcément ça donne 0 (cette méthode prend moins d'octets que de faire LD A, $0). Ensuite on y retrouve un label local .wait (Sa portée n'est pas globale et est active uniquement dans son label parent) qui va nous servir de marqueur pour définir notre boucle, HALT permet de "mettre en veille" le processeur jusqu'à ce qu'une interruption s'effectue (Ça nous fait économiser les piles) et on effectue une comparaison du contenu de HL sur l'accumulateur, si le résultat équivaut à 0 le drapeau Z est activé, jr on l'a vu tout à l'heure, on lui a juste ajouté une condition (Saute à .wait uniquement si le drapeau Z est actif) puis on va passer notre variable vblank_flag à 0 (Pour rappel on attend la période V-Blank) et enfin ret pour sortir de la fonction.

On retourne à notre label "main:" et on va enfin pouvoir commencer les choses sérieuses !

main:
    ld a, %00000001
    ldh [$FF], a ; INT V-Blank
    ei
    
    ; Attendre le V-Blank et éteindre l'écran
    call wait_vblank
    ld a, %00010000
    ldh [$40], a
    
    .no_op
    halt
    jr .no_op

On va activer le bit 0 (C'est lui qui gère l'interruption V-Blank) puis tout envoyer dans $FFFF (Le registre mémoire qui gère les interruptions) puis on utilise l'opérande ei pour activer les interruptions au niveau du processeur.

On fait ensuite appel à notre fonction wait_vblank et on active le bit 4 (Pour activer la plage $8000-$8FFF, inutile normalement vu qu'on éteint l'écran mais dans notre cas ça sera utile), on peut tout envoyer dans le registre mémoire qui gère l'affichage ($FF40). Voilà notre écran est éteint ! Le programme contient ensuite notre petite boucle de glandouille pour éviter de planter.

La visionneuse de VRAM de BGB qui montre les tuiles du logo Nintendo

Si vous ouvrez la visionneuse de VRAM de BGB vous pourrez constater que l'écran est bien éteint mais que les tuiles restent toujours sur le background, il faut donc le nettoyer.

main:
    [...]
    ldh [$40], a

    ; Effacer le background
    ld de, 32*32
    ld hl, $9800
    .clear_mem
        xor a
        ld [hli], a
        dec de
        ld a, d
        or e
        jr nz, .clear_mem
            
    .no_op
    halt
    jr .no_op

On écrit le résultat de 32*32 dans le registre DE (L'assembleur (RGBASM) va effectuer le calcul en amont, le processeur n'est pas capable de faire une opération mathématique avec LD) puis on pointe l'emplacement mémoire $9800 (Qui correspond à notre map) dans HL.

Notre bon pote le label local .clear_mem pour gérer notre boucle est présent puis on effectue un OU exclusif sur l'accumulateur afin de le passer à 0 et on peut écrire ce 0 dans $9800 puis incrémenter le registre HL pour arriver à $9801 avec hli (HL + INC), on décrémente ensuite le registre DE pour passer de 1024 à 1023 (Vous l'aurez compris, on va itérer la boucle 1024 fois).

On va ensuite envoyer le contenu du registre D dans l'accumulateur afin de pouvoir faire notre OU inclusif (Pas possible d'activer le drapeau Z si on compare directement sur un registre 16 bits, il faut donc ruser), avec jr nz, tant que le drapeau Z n'est pas actif la boucle va continuer. Si tout est OK on retourne à notre label ".no_op" et notre map est toute propre, prête à accueillir nos plus belles tuiles.

On va retourner quelques instants dans la visionneuse de VRAM de BGB pour s'intéresser aux palettes de "couleurs".

La visionneuse de VRAM de BGB qui montre les différentes palettes

On va juste se préoccuper de la palette BGP (Il n'y en a bien qu'une malgré ce qui est affiché). Quand le bootstrap de la GameBoy se lance il remplit les 3 dernières couleurs de noir, on doit donc initialiser la palette par défaut et ça se passe juste en dessous de ".no_op".

    ; Palette par défaut
    ld a, %11100100
    ldh [$47], a

On envoie dans l'accumulateur les couleurs de la palette par défaut et on les inscrit dans le registre mémoire $FF47, c'est lui qui gère la (seule) palette principale.

On va lâcher le clavier pendant un instant (ouf) et prendre la souris pour faire de beaux dessins (Plutôt de belles lettres), vous vous souvenez de GBTD ? On en a parlé plus haut, eh bien on va devoir s'en servir.

GBTD en action

Le principe ici c'est de dessiner des lettres afin d'écrire ce que vous voulez, ce n'est pas optimal d'écrire du texte de cette manière car il y a des répétitions de caractères mais c'est juste pour l'exemple.

Fenêtre d'exportation de GBTD

On peut ensuite exporter nos tuiles (Files => Export to) en tant que fichier binaire de 0 jusqu'au nombre de la dernière tuile dessinée (Ici c'est 10 donc on met 10).

De retour (parce qu'on revient !) sur le clavier, on écrit ce bout de code à la toute fin de notre fichier main.asm.

tuiles:
    incbin "tuiles.bin"

Cette ligne va inclure directement notre fichier binaire dans la ROM, on va mettre ça dans la VRAM.

main:
    [...]
    ; Palette par défaut
    ld a, %11100100
    ldh [$47], a
    
    ; Ajout des tuiles dans la VRAM
    ld hl, tuiles
    ld de, $8010
    ld bc, 11*16
    .copy_mem
        ld a, [hli]
        ld [de], a
        inc de
        dec bc
        ld a, b
        or c
        jp nz, .copy_mem
        
    .no_op
    halt
    jr .no_op

Une boucle basique qui a déjà été expliqué plus haut. Le registre DE pointe sur l'adresse mémoire où on va mettre notre première tuile et le registre BC est un compteur d'itération (On dispose de 11 tuiles qui font 16 octets chacunes), petite subilité avec le registre HL, il pointe sur l'adresse mémoire où est notre fichier binaire.

$8000 correspond au fond de notre background, on doit le laisser vierge

Juste en dessous on peut mettre la fonction qui va ajouter ces tuiles sur la map.

    ; Ajout des tuiles sur la map
    ld b, $01
    ld de, $9904
    ld c, 11
    .copy_mem2
        ld a, b
        ld [de], a
        inc b
        inc de
        dec c
        xor a
        cp c
        jp nz, .copy_mem2

Le registre B correspond à notre premier numéro de tuile, on va l'incrémenter au fur et à mesure. Le registre DE est l'emplacement où le texte sera affiché sur la map, en passant le curseur sur "BG Map" dans la visionneuse de VRAM de BGB on peut savoir l'adresse mémoire souhaitée. Le registre C est un compteur d'itération.

Bon on a bientôt fini, il ne nous reste plus qu'à allumer l'écran.

    ; Allumer l'écran
    ld a, %10010001 ; LCD ON, Tile Map = $9800, Window OFF, BG Tile Data = $8000, BG Tile Map = 9800, Sprite 8x8, Sprite OFF, BG ON
    ldh [$40], a
    
    .no_op
    halt
    jr .no_op

On va activer le bit 7 et le bit 0 pour allumer le LCD et le background puis on envoie le tout dans $FF40. Plus qu'à assembler tout le délire et notre première ROM pour la GameBoy s'illumine sous nos yeux :)

Le résultat du programme sur BGB

Vous avez maintenant les bases pour développer pour la GameBoy et le tout en assembleur (Si c'est pas la classe ça).

Je vous mets bien entendu à disposition les sources du projet qu'on a réalisé durant cet article ainsi que la ROM (qui fonctionne sur une vraie GameBoy :)).


Partager sur Twitter Partager sur Facebook