Introduction à la programmation sur GameBoy
Introduction de l'introduction
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 :
- Z S'active quand le dernier résultat est égal à 0
- N S'active quand la dernière opération utilisée sur l'accumulateur est une soustraction
- H Drapeau de demi-retenue
- C S'active quand le dernier résultat est un dépassement (retenue)
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).
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.
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".
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.
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.
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.
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 :)
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 :)).