Pitt
11/03/2007, 21h23
Programmer sur DS avec la libnds
Version 20070427
par Pitt
## Partie A, Théorie
>> Sommaire
0. Introduction
1. Console, texte
2. Le hardware de la DS
2.1 Présentation
2.2 Connexion sans fil
2.3 Contrôles/sorties
2.4 Mémoire
3. Touches, pad et stylet
3.1 Touches et pad
3.2 Stylet
4. Vidéo 2D
4.1 "Power management"
5. Vidéo 3D
6. Direct Memory Access
7. Timers
8. Interruptions
8.1 Interruptions hardwares
8.2 Interruptions softwares
9. Inter Processor Communication (IPC)
10. Mots de la fin
10.1 Remerciements
10.2 Liens
0. Introduction
Ce tutoriel s'adresse à tous ceux qui souhaitent se lancer dans la programmation sur DS à l'aide, ou pas, de la libnds. Il vise plus à servir de référence qu'à initier, donc une expérience en programmation sur GBA, ou sur DS avec la PAlib constitue un atout non négligeable pour en débuter la lecture. J'essayerai d'être le plus court et concis possible, tout en restant relativement précis.
Comme son nom ne l'indique pas, ce tutoriel n'est pas destiné qu'à ceux qui veulent utiliser la libnds : j'essayerai d'ajouter le plus possible les adresses des registres et les différentes constantes et autres bits, pour permettre à ceux qui voudraient se débrouiller seuls ou seulement comprendre un peu mieux le hardware DS, d'avoir une documentation relativement précise et pas trop barbante ...
Pour le moment, ce tutoriel n'explique pas comment installer devkitpro et libnds, mais c'est tellement simple, et il y a suffisamment de bons tutoriels à ce sujet sur PA et sur le web pour que j'évite de le faire. Une fois ce document terminé, je le rajouterai peut-être, mais pas avant que j'aie mis toutes les choses utiles. Au cas où, vous avez les liens à la fin du tutoriel.
Bonne lecture !
N.B.: Sauf indication contraire, vous pouvez utiliser le template arm9 des exemples de la libnds pour tester les notions abordées dans ce tutoriel.
1. Console, texte
Avant d'entrer dans les détails techniques, il peut être sympathique d'aborder de façon simple le développement sur DS. Cette première partie montre donc brièvement comment afficher du texte grâce à une console par défaut, initialisée par la libnds.
Certaines notions peuvent vous être complètement inconnues, surtout si vous n'avez jamais programmé sur GBA. Mais le but de cette première partie est justement d'afficher un petit quelque chose, pour le plaisir de ceux qui débutent.
Vous pourrez donc revenir à cette partie plus tard, notamment après avoir lu la partie "Vidéo 2D" de ce tutoriel, pour mieux comprendre le pourquoi du comment.
Voici donc un petit Hello world commenté, qui vous permettra d'afficher du texte, même sans vraiment comprendre comment cela fonctionne réellement :
#include <nds.h>
#include <stdio.h>
int main(void)
{
/**
* Il n'est pas nécessaire de comprendre cette première partie,
* sauf si vous avez déjà lu le tutoriel en son ensemble.
* Donc débutant, passez directement à la case 7.
*/
/* 1) Initialisation des interruptions */
irqInit();
irqSet(IRQ_VBLANK, 0);
/* 2) On initialise l'écran supérieur, mais pas
* le tactile, étant donné qu'il nous est inutile. */
videoSetMode(0);
videoSetModeSub(MODE_0_2D | DISPLAY_BG0_ACTIVE);
/* 3) On active la bonne banque de données sur notre écran */
vramSetBankC(VRAM_C_SUB_BG);
/* 4) On configure l'adresse de la carte de notre fond */
SUB_BG0_CR = BG_MAP_BASE(31);
/* 5) La couleur du texte correspond à la couleur 255
* de la palette ; on utilise la macro RGB() */
BG_PALETTE_SUB[255] = RGB15(31, 31, 31);
/* 6) On initialise notre console */
consoleInitDefault((u16 *) SCREEN_BASE_BLOCK_SUB(31), (u16 *) CHAR_BASE_BLOCK_SUB(0), 16);
/* 7) On affiche du texte avec la fonction iprintf,
* qui prend comme unique argument le texte à afficher */
iprintf("Hello world !\n");
/* 8) Une boucle infinie, pour éviter de freezer,
* ça serait dommage ... */
while (1);
return 0;
}
Cette première partie n'est finalement qu'un aperçu pour les plus pressés, et elle n'apprend pas grand chose ... La troisième partie traite des différentes entrées basiques de la DS. Mais avant cela, il faut bien parler du hardware de la DS.
2. Le hardware de la DS
2.1 Présentation
La Nintendo Dual Screen, ou DS, est une console portable. Je précise au cas où vous ne seriez pas au courant ... Développée par Nintendo, elle est dotée de 2 processeurs, de la même famille, un ARM946E-S, cadencé à 67 MHz, et un ARM7TDMI, cadencé à 33 MHz. Pour le moment, nous n'utiliserons "que" l'ARM9, pour la simple et bonne raison que c'est celui qui nous permet d'utiliser les fonctions principales de la DS. Mais par la suite, nous utiliserons le second processeur.
2.2 Connexion sans fil
La DS gère la communication sans fil de 2 manières : en utilisant la norme IEEE 802.11 pour la connexion à internet, et un format propriétaire Nintendo, le NiFi, pour communiquer entre DS.
La première méthode peut être gérée via la libwifi, mais il n'y a encore aucune librairie fonctionnelle permettant d'utiliser le NiFi.
2.3 Contrôles/sorties
La DS est dotée de 2 écrans, dont un tactile. Ils ont tous les deux une résolution de 256x192 pixels, un pitch de 0,24 mm et ils gèrent 262 000 couleurs.
En plus de l'écran tactile, la DS possède de nombreux contrôles : une croix multidirectionnelle, et 8 boutons X, Y, A, B, L, R, Start et Select. Elle dispose également d'un micro intégré, en dessous de l'écran tactile pour les DS, et entre les deux écrans pour la DS Lite.
Enfin, elle dispose de 2 haut-parleurs stéréo.
2.4 Mémoire
La DS est dotée de 4 Mo de mémoire RAM. De plus, l'ARM7 peut accéder à une mémoire de 64 ko, et la console possède également une zone de mémoire partagée par l'ARM9 et l'ARM7, de 32 ko. Enfin, une mémoire de 656 ko est réservée à la vidéo, c'est-à-dire à l'unité graphique de la DS.
Le tableau suivant montre les types de données utilisés par la DS, et leurs equivalents utilisés par GCC :
Nom|Taille en bits
{colsp=2}Types génériques
Nibble|4
Octet (byte)|8
Demi-mot (halfword ou hword)|16
Mot (word)|32
Double-mot (doubleword ou dword)|64
{colsp=2}Types utilisés par GCC
char|8
short|16
long|32
long long|64
float|32
double|64
Voici également un tableau des différents types déclarés par la libnds :
Non-signé|Signé|Volatile non-signé|Volatile signé|Taille en bits|Abbréviations
bool|-|-|-|8|-
uint8|int8|vuint8|vint8|8|u8, s8, vu8, vs8
uint16|int16|vuint16|vint16|16|u16, s16, vu16, vs16
uint32|int32|vuint32|vint32|32|u32, s32, vu32, vs32
uint64|int64|vuint64|vint64|64|u64, s64, vu64, vs64
-|float32|-|vfloat32|32|-
-|float64|-|vfloat64|64|-
De plus, la DS est Little Endian, donc les octets qui composent chaque mot sont inversés. Par exemple le double-mot 0xBADC0FEE sera stocké comme suit :
Octet|3|2|1|0
Valeur|BA|DC|OF|EE
3. Touches, pad et stylet
La gestion des touches, du pad et du stylet est extrêmement simple sur DS. Cette troisième partie passe rapidement en revue les registres, et les fonctions de la libnds, servant à l'utilisation des entrées de la DS, du moins au niveau des doigts ...
3.1 Touches et pad
La DS stocke l'état actuel des différentes touches dans le registre REG_KEYINPUT, registre 16 bits situé à l'adresse 0x04000130.
Bit|13|12|11|10|9|8|7|6|5|4|3|2|1|0
Nom|KEY_LID|KEY_TOUCH|KEY_Y|KEY_X|KEY_L|KEY_R|KEY_ DOWN|KEY_UP|KEY_LEFT|KEY_RIGHT|KEY_START|KEY_SELEC T|KEY_B|KEY_A
Processeur|ARM7|ARM7|ARM7|ARM7|Les 2|Les 2|Les 2|Les 2|Les 2|Les 2|Les 2|Les 2|Les 2|Les 2
Représente|Diode|Ecran tactile|Bouton Y|Bouton X|Bouton L|Bouton R|Bas|Haut|Gauche|Droite|Start|Select|Bouton B|Bouton A
Il suffit donc de lire le contenu de ce registre, et d'utiliser des masques de bits pour détecter quelles touches sont pressées. Certains des bits ne sont actualisés que pour l'ARM7, mais cela importe peu quand on programme à l'aide de la libnds, car celle-ci fait passer le contenu de REG_KEYINPUT à l'ARM9 via l'IPC (voir plus loin).
La libnds propose plusieurs déclarations concernant la lecture des touches :
Une fonction permettant de récupérer le contenu du registre REG_KEYINPUT dans un buffer.
void scanKeys();
2 fonctions permettant de savoir, une fois le contenu de REG_KEYINPUT mis en buffer, quelles touches sont respectivement :
- maintenues appuyées
- pressées
- relevées
depuis le dernier appel à scanKeys().
uint32 keysHeld(void);
uint32 keysDown(void);
uint32 keysUp(void);
/!\ Si le joueur reste appuyé sur un bouton, keysDown() renverra 0 pour ce bouton. C'est alors keysHeld() qui prendra le relais.
il ne faut donc pas confondre ces 2 fonctions, et distinguer la différence entre une touche relevée, pressée ou maintenue.
2 fonctions permettant respectivement de détecter la répétition d'un appui sur une touche, et de paramètrer la répétition des touches.
uint32 keysDownRepeat(void);
/**
* setDelay est le nombre d'appels à scanKeys() avant que les touches commencent à se répéter.
* setRepeat est le nombre d'appels à scanKeys() avant que les touches ne se répètent.
*/
void keysSetRepeat( u8 setDelay, u8 setRepeat );
Une énumération représentant la valeur des différentes touches.
typedef enum KEYPAD_BITS {
/* Touches */
KEY_A = BIT(0),
KEY_B = BIT(1),
KEY_SELECT = BIT(2),
KEY_START = BIT(3),
KEY_RIGHT = BIT(4),
KEY_LEFT = BIT(5),
KEY_UP = BIT(6),
KEY_DOWN = BIT(7),
KEY_R = BIT(8),
KEY_L = BIT(9),
KEY_X = BIT(10),
KEY_Y = BIT(11),
/* Ecran tactile */
KEY_TOUCH = BIT(12),
/* Etat de la diode */
KEY_LID = BIT(13)
} KEYPAD_BITS;
Un usage fréquent de ces fonctions est présenté ci-dessous :
scanKeys();
if (keysDown() & KEY_A)
{
/* si le bouton A est pressé ...*/
}
3.2 Stylet
Pour savoir si le stylet est appuyé sur l'écran, il suffit d'effectuer un scanKeys(), puis de tester keysDown() avec le masque de bit KEY_TOUCH.
Pour connaitre l'endroit où le stylet est appuyé, il faut utiliser la fonction suivante, qui retourne une structure touchPosition. Cette structure contient 6 champs, qui sont expliqué plus bas.
touchPosition touchReadXY();
/* La structure touchPosition */
typedef struct touchPosition {
int16 x; /* Coordonnée X de l'endroit où l'écran est touché */
int16 y; /* Coordonnée Y de l'endroit où l'écran est touché */
int16 px; /* Coordonnée X du pixel à l'endroit où l'écran est touché */
int16 py; /* Coordonnée Y du pixel à l'endroit où l'écran est touché */
int16 z1; /* FIXME : profondeur de l'appui ? */
int16 z2; /* FIXME : profondeur de l'appui ? */
} touchPosition;
Cette structure est passée à l'ARM9 par l'ARM7 via une zone commune aux 2 processeurs, qui permet d'échanger des informations entre eux, l'IPC. Cette zone est expliquée plus en détail dans la partie 9, et ce sera par ailleurs l'occasion d'expliquer plus en détail les registres qui concernent la récupération des coordonnées du stylet, de la pression des touches X et Y, et de l'écran tactile.
4. Vidéo 2D
La DS est dotée d'un hardware relativement bien fourni concernant la 2D : il existe de multiples façons d'accéder aux écrans, et d'y afficher ce que l'on désire.
Il faudra bien comprendre dans cet partie la différence entre l'écran principal, généralement (mais pas tout le temps) utilisé en bas, et l'écran secondaire. Pourquoi cette distinction ? Parce qu'on peut échanger la place des 2 écrans, ce qui peut fausser toutes vos fonctions d'affichage si vous n'y pensez pas. De plus, la section suivante s'intéresse au hardware 3D, qui n'est utilisable QUE sur l'écran principal.
4.1 "Power management"
Tout d'abord, l'unité LCD est régie par un registre de gestion de l'alimentation, qui permet d'activer, ou non, les 2 écrans, mais aussi de déterminer quelles opérations sont activées.
C'est le registre 16bits REG_POWERCNT, situé à l'adresse 0x04000304.
Le tableau suivant récapitule les différents bits activables de REG_POWERCNT :
Bit|Define de la libnds|Action
0|POWER_LCD|Active les 2 écrans LCD
1|POWER_2D_A|Active la 2D sur l'écran principal
2|POWER_MATRIX|Active les matrices 3D
3|POWER_3D_CORE|Active la 3D sur l'écran principal
9|POWER_2D_B|Active la 2D sur l'écran secondaire
15|POWER_SWAP_LCDS|Echange les 2 écrans
Afin de simplifier la vie des codeurs, la libnds contient 2 macros supplémentaires :
- POWER_ALL_2D, qui active toute la 2D (écrans, 2D sur chaque écran)
- POWER_ALL, qui active tout (écrans, 2D sur chaque écran, 3D)
La libnds fournit plusieurs fonctions concernant le "power management" :
Une fonction qui ajoute les paramètres qu'on lui donne
static inline void powerON(int on);
Une fonction qui n'active QUE les paramètres qu'on lui donne
static inline void powerSET(int on);
Une fonction qui désactive les paramètres qu'on lui donne
static inline void powerOFF(int off);
3 fonctions de gestion de la position des écrans
/* - Echange les 2 écrans */
static inline void lcdSwap(void);
/* - Mettre l'écran principal en haut */
static inline void lcdMainOnTop(void);
/* - Mettre l'écran principal en bas */
static inline void lcdMainOnBottom(void);
Deux utilisations typiques sont présentées ci-dessous :
/* - utilisation de la 2D uniquement */
powerON(POWER_ALL_2D);
/* - utilisation de la 2D et de la 3D */
powerON(POWER_ALL);
5. Vidéo 3D
6. Direct Memory Access
Pour transférer de manière rapide, on peut utiliser une technique spéciale, appelée Direct Memory Access, ou DMA. Comme son nom ne l'indique qu'à moitié, cette méthode demande au processeur de stopper l'exécution du programme et de transférer directement des données en mémoire. Cela peut avoir des avantages considérables, surtout lors de l'utilisation de modes graphiques bitmaps, ou encore pour transférer des sons vers les hauts-parleurs. Mais en contre partie, le DMA stoppe l'exécution du processeur, ce qui peut être vu comme un inconvénient.
La DS possède 4 canaux de transfert DMA, qui sont utilisables grâce aux registres DMA_CR(x), DMA_SRC(x) et DMA_DEST(x), où x est le numéro de canal compris entre 0 et 3. C'est 4 canaux ne diffèrent que par l'usage qu'on leur réserve généralement :
- canal 0 : le plus rapide, surtout utilisé pour les transferts critiques.
- canaux 1 et 2 : utilisés pour le transfert du son.
- canal 3 : les autres transferts.
Ces indications sont bien entendues théoriques ; le canal 0 peut très bien être utilisé pour transfèrer du son, et on peut très bien copier une structure avec le canal 1.
Le premier registre, DMA_CR(x), permet le contrôle du transfert DMA ; le tableau suivant récapitule les paramètres qui peuvent être utilisés pour le contrôle d'un transfert, via le registre DMA_CR(x) qui est un registre 32 bits situé à l'adresse :
- 0x040000B0 pour le canal 0.
- 0x040000BC pour le canal 1.
- 0x040000C8 pour le canal 2.
- 0x040000D4 pour le canal 3.
Paramètre|Bit|Processeur|Action
{colsp=2}Activation du transfert
DMA_ENABLE|31|ARM7 & ARM9|Active le transfert DMA sur ce canal.
DMA_IRQ_REQ|30|ARM7 & ARM9|Active la génération d'une interruption à la fin du transfert.
DMA_START_NOW|-|ARM7 & ARM9|Commence le transfert DMA immédiatement.
DMA_START_CARD|-|ARM7 & ARM9|
DMA_START_VBL|27|ARM7 & ARM9|Commence le transfert DMA au prochain VBL.
DMA_START_HBL|28|ARM9|Commence le transfert DMA au prochain HBL.
DMA_START_FIFO|-|ARM9|
DMA_DISP_FIFO|-|ARM9|
{colsp=2}Tailles de transfert
DMA_16_BIT|-|ARM7 & ARM9|Transfert de demi-mots (halfwords = 16 bits).
DMA_32_BIT|26|ARM7 & ARM9|Transfert de mots (words = 32 bits).
{colsp=2}Types de transfert
DMA_REPEAT|25|ARM7 & ARM9|
DMA_SRC_INC|-|ARM7 & ARM9|Récupère les données à copier de manière ascendante.
DMA_SRC_DEC|23|ARM7 & ARM9|Récupère les données à copier de manière descendante.
DMA_SRC_FIX|24|ARM7 & ARM9|Copie toujours le même demi-mot/mot.
DMA_DST_INC|-|ARM7 & ARM9|Copie les données de manière ascendante.
DMA_DST_DEC|21|ARM7 & ARM9|Copie les données de manière descendante.
DMA_DST_FIX|22|ARM7 & ARM9|Copie toujours au même endroit.
DMA_DST_RESET|-|ARM7 & ARM9|
{colsp=2}Transferts prédéfinis
DMA_COPY_WORDS|-|Transfert immédiat de mots.
DMA_COPY_HALFWORDS|-|Transfert immédiat de demi-mots.
DMA_FIFO|-|
Le bit 31 (= DMA_BUSY) du registre DMA_CR(x) indique, une fois le transfert commencé, si le canal est occupé ou non. La libnds fournit également la fonction qui suit, et qui permet de savoir si le canal DMA choisi est libre, ou pas :
static inline int dmaBusy(uint8 channel);
Le second registre, DMA_SRC(x), contient l'adresse des données à copier.
C'est également un registre 32 bits, situé à l'adresse :
- 0x040000B4 pour le canal 0.
- 0x040000C0 pour le canal 1.
- 0x040000CC pour le canal 2.
- 0x040000D8 pour le canal 3.
Le troisième registre, DMA_DEST(x), contient l'adresse à laquelle les données seront transférées.
Celui-ci est encore une fois un registre 32 bits, situé à l'adresse :
- 0x040000B8 pour le canal 0.
- 0x040000C4 pour le canal 1.
- 0x040000D0 pour le canal 2.
- 0x040000DC pour le canal 3.
La libnds fournit plusieurs fonctions de transferts prédéfinies :
Transferts synchrones
/* - de mots */
static inline dmaCopyWords(uint8 channel, const void* src, void* dest, uint32 size);
/* - de demi-mots */
static inline void dmaCopyHalfWords(uint8 channel, const void* src, void* dest, uint32 size);
Transferts Asynchrones
/* - de mots */
static inline void dmaCopyWordsAsynch(uint8 channel, const void* src, void* dest, uint32 size);
/* - de demi-mots */
static inline void dmaCopyHalfWordsAsynch(uint8 channel, const void* src, void* dest, uint32 size);
Transferts "à la memcopy", utilisant le canal 3
/* - synchrone */
static inline void dmaCopy(const void * source, void * dest, uint32 size);
/* - asynchrone */
static inline void dmaCopyAsynch(const void * source, void * dest, uint32 size);
Sinon, une utilisation typique du DMA se déroule comme suit :
/* Version synchrone */
DMA_SRC(canal) = adresse_source;
DMA_DEST(canal) = adresse_destination;
DMA_CR(canal) = parametres | taille;
while (dmaBusy(canal));
/* Version asynchrone */
DMA_SRC(canal) = adresse_source;
DMA_DEST(canal) = adresse_destination;
DMA_CR(canal) = parametres | taille;
7. Timers
Un timer est un registre compteur, qui joue le rôle d'horloge. Il permet par exemple d'attendre un certain temps, ou d'effectuer une action à intervalles réguliers. La DS possède 4 timers, numérotés de 0 à 3, qui sont tous les quatre cadencés à 33.514 MHz. L'utilisation de ces derniers est très simple, et s'effectue grâce à 2 registres.
Le premier, TIMER_CR(x), où x est le numéro du timer, permet de gérer, comme pour les transferts DMA, la configuration et la mise en place du timer. Le tableau qui suit montre les différents paramètres que l'on peut sélectionner. Le registre TIMER_CR(x) est un registre 16 bits, qui se situe à l'adresse :
- 0x04000102 pour le timer 0.
- 0x04000106 pour le timer 1.
- 0x0400010A pour le timer 2.
- 0x0400010E pour le timer 3.
Paramètre|Bit|Action
TIMER_ENABLE|7|Active le timer.
TIMER_IRQ_REQ|6|Active la génération d'une interruption lors du débordement du timer.
TIMER_CASCADE|2|Active la mise en cascade des timers, c'est-à-dire que le timer ne s'active que lorsque le timer précédent déborde. Ne peut être utilisé sur le timer 0.
TIMER_DIV_1|0-1|Cadence le timer à 33.514 MHz.
TIMER_DIV_64|0-1|Cadence le timer à (33.514 / 64) MHz.
TIMER_DIV_256|0-1|Cadence le timer à (33.514 / 256) MHz.
TIMER_DIV_1024|0-1|Cadence le timer à (33.514 / 1024) MHz.
Le second registre, TIMER_DATA(x) possède un comportement un peu plus compliqué. En effet, il permet en écriture de sélectionner la fréquence du compteur, grâce aux macros présentées dans le tableau suivant, qui sont utilisées suivant la division de la cadence du timer. Mais en lecture, il renvoit la valeur actuelle du compteur.
Ce registre de 16 bits se situe à l'adresse :
- 0x04000100 pour le timer 0.
- 0x04000104 pour le timer 1.
- 0x04000108 pour le timer 2.
- 0x0400010C pour le timer 3.
Paramètre de division du timer|Macro pour le choix de la fréquence x
TIMER_DIV_1|TIMER_FREQ(x)
TIMER_DIV_64|TIMER_FREQ_64(x)
TIMER_DIV_256|TIMER_FREQ_256(x)
TIMER_DIV_1024|TIMER_FREQ_1024(x)
La libnds ne fournit aucune fonction de gestion de timers, mais leur utilisation est très simple, et ce fait comme suit :
/* En utilisant la bonne macro pour calculer la fréquence */
TIMER_CR(numero) = parametres;
TIMER_DATA(numero) = frequence;
8. Interruptions
8.1 Interruptions hardwares
Le processeur ARM9 utilisé jusqu'ici, tout comme l'ARM 7 d'ailleurs, lit les instructions du programme courant les unes après les autres, et les exécute. Il est relié à différents périphériques, comme par exemple les touches ou les hauts-parleurs.
Pour échanger des informations entre les périphériques, on peut utiliser 2 méthodes :
- la première, appelée polling consiste à attendre dans une boucle infinie qu'une condition soit remplie, comme par exemple la pression du bouton R. Cette méthode a ses avantages, mais aussi ses inconvénients, vus qu'on ne peut pas vraiment effectuer de longues tâches pendant la période d'attente.
- la seconde consiste à utiliser un système d'interruptions. Une interruption, au sens électronique du terme, est un simple fil, qui relie un périphérique au processeur. Cet entrée conserve toujours la même valeur, soit 0 ou 1. Mais lorsque le périphérique a besoin d'informer le programme d'un évènement, il change la valeur de l'entrée. Le processeur interrompt alors l'exécution du programme, et exécute la routine vers laquelle il est configuré pour pointer lors de l'interruption. Une fois cette routine exécutée, l'entrée reprend sa valeur initiale, et l'exécution du programme continue là où elle s'était arrêtée.
Tout comme le polling, cette technique possède des avantages et des inconvénients, mais les interruptions possèdent un panel très varié : touches pressées, évènement Wifi, fin de transfert DMA ...
La DS possède 23 interruptions ; le tableau suivant les liste, indique le processeur qui y a accès, et donne une brève description de ces dernières.
Nom|Bit|Processeur|Description
IRQ_VBLANK|0|ARM9 & ARM7|Générée lors de la période de VBlank.
IRQ_HBLANK|1|ARM9 & ARM7|Générée lors de la période de HBlank.
IRQ_VCOUNT|2|ARM9 & ARM7|Générée lorsque la valeur du VCOUNT correspond à la valeur configurée dans DISPSTAT.
IRQ_TIMER0|3|ARM9 & ARM7|Générée lors du débordement du timer 0.
IRQ_TIMER1|4|ARM9 & ARM7|Générée lors du débordement du timer 1.
IRQ_TIMER2|5|ARM9 & ARM7|Générée lors du débordement du timer 2.
IRQ_TIMER3|6|ARM9 & ARM7|Générée lors du débordement du timer 3.
IRQ_NETWORK|7|ARM7|
IRQ_DMA0|8|ARM9 & ARM7|Générée lors de la fin d'un transfert DMA sur le canal 0.
IRQ_DMA1|9|ARM9 & ARM7|Générée lors de la fin d'un transfert DMA sur le canal 1.
IRQ_DMA2|10|ARM9 & ARM7|Générée lors de la fin d'un transfert DMA sur le canal 2.
IRQ_DMA3|11|ARM9 & ARM7|Générée lors de la fin d'un transfert DMA sur le canal 3.
IRQ_KEYS|12|ARM9 & ARM7|Générée lorsque la valeur des touches pressées est égale à la valeur stockée dans REG_KEYCNT.
IRQ_CART|13|ARM9 & ARM7|Générée par la cartouche GBA.
IRQ_IPC_SYNC|16|ARM9 & ARM7|Générée lors de la synchronisation de l'IPC.
IRQ_FIFO_EMPTY|17|ARM9 & ARM7|Générée lors de l'envoi d'un FIFO vide.
IRQ_FIFO_NOT_EMPTY|18|ARM9 & ARM7|Générée lors de la réception d'un FIFO vide.
IRQ_CARD|19|ARM9 & ARM7|Générée à la fin d'un transfert de données depuis la carte.
IRQ_CARD_LINE|20|ARM9 & ARM7|
IRQ_GEOMETRY_FIFO|21|ARM9|Interruption géométrie FIFO.
IRQ_LID|22|ARM7|
IRQ_SPI|23|ARM7|Interruption du bus SPI.
IRQ_WIFI|24|ARM7|Générée lorsqu'un évènement Wifi se produit.
IRQ_ALL|~0|ARM9 & ARM7|L'ensemble des interruptions.
On peut activer chaque interruption en activant le bit correspondant dans le registre REG_IE, registre 32 bits situé à l'adresse 0x04000210.
De plus, il peut être utile de désactiver momentanément toutes les interruptions. Pour cela, le bit 0 du registre REG_IME, registre 16 bits situé à l'adresse 0x04000208, permet de désactiver toutes les interruptions lorsqu'il est mis à 0, que le bit de l'interruption soit activé ou non dans le registre REG_IE.
La libnds propose plusieurs fonctions pour gérer les interruptions :
une fonction d'initialisation globale
void irqInit();
2 fonctions permettant respectivement d'associer une fonction à une interruption, ou de supprimer la fonction associée à cette interruption
/* Associer : */
void irqSet(IRQ_MASK irq, VoidFunctionPointer handler);
/* Effacer : */
void irqClear(IRQ_MASK irq);
2 fonctions, permettant respectivement d'activer ou de désactiver une interruption
/* Activer : */
void irqEnable(IRQ_MASK irq);
/* Désactiver : */
void irqDisable(IRQ_MASK irq);
une fonction permettant d'utiliser un "interrupt handler" différent de celui utilisé par la libnds
void irqInitHandler(VoidFunctionPointer handler);
8.2 Interruptions softwares
Dans la partie précédente, j'ai usé et abusé du terme d'interruption. en réalité, c'est un abus de langage, et les interruptions précédentes étaient en réalité des interruptions hardwares. Il existe un autre type d'interruptions, qui fonctionnent comme les interruptions hardware, mais qui sont déclenchées par le programme lui-même. On appelle ces interruptions interruptions softwares (abréviation : SWI, pour SoftWare Interrupt).
Pour utiliser une interruption software, il suffit d'utiliser l'instruction ARM swi. Même s'il est possible d'utiliser cette instruction en C via asm(), on préfèrera utiliser les fonctions libnds correspondantes, lorsqu'elles existent. La DS possède 25 interruptions softwares, que le tableau suivant récapitule, tout en leur associant les fonctions libnds qui leur correspondent.
Valeur|Processeur|Fonction|Fonction libnds|Explication des paramètres
00h|ARM9 & ARM7|Effectue un reset software de la DS.|void swiSoftReset(void);|-
03h|ARM9 & ARM7|Attends un certain temps.|void swiDelay(uint32 duration);|duration est la durée à attendre.
04h|ARM9 & ARM7|Attends une interruption hardware.|void swiIntrWait(int waitForSet, uint32 flags);|Si waitForSet vaut 0, retourne si l'interruption a déjà eu lieu. Si waitForSet vaut 1, attend que l'interruption ait lieu. flags est l'interruption à attendre.
05h|ARM9 & ARM7|Attends l'interruption VBL.|void swiWaitForVBlank(void);|-
06h|ARM9 & ARM7|Stoppe le processeur|void swiHalt(void);|-
07h|ARM7|Mise en veille|void swiSleep(void);|-
08h|ARM7|change le registre SOUNDBIAS|void swiChangeSoundBias(int enabled, int delay);|enabled est le niveau de BIAS, delay est le délai (ex.: 8 sur GBA).
09h|ARM9 & ARM7|Division et modulo signé|int swiDivide(int numerator, int divisor);| numerator est le numérateur, et divisor le diviseur. Retourne le quotient.
Idem|Idem|Idem|int swiRemainder(int numerator, int divisor);| numerator est le numérateur, et divisor le diviseur. Retourne le modulo.
0Bh|ARM9 & ARM7|Copie|void swiCopy(const void * source, void * dest, int flags);|Les bits 0-20 de flags correspondent à la taille du transfert, le bit 24 au type de transfert (COPY_MODE_COPY ou COPY_MODE_FILL, copie ou remplissage) et le bit 26 à la taille du transfert(COPY_MODE_HWORD ou COPY_MODE_WORD, demi-mot ou mot).
0Ch|ARM9 & ARM7|Copie rapide|void swiFastCopy(const void * source, void * dest, int flags);|Les bits 0-20 de flags correspondent à la taille du transfert et le bit 24 au type de transfert (COPY_MODE_COPY ou COPY_MODE_FILL, copie ou remplissage).
0Dh|ARM9 & ARM7|Racine carrée|int swiSqrt(int value);|value est le nombre dont on cherche la racine carrée.
0Eh|ARM9 & ARM7|Retourne le CRC-16|uint16 swiCRC16(uint16 crc, void * data, uint32 size);|crc est le CRC-16 initial, data est un pointeur vers les données, et size est la taille des données en octets.
0Fh|ARM9 & ARM7|Retourne 1 si le programmer tourne sur debuggeur hardware.|int swiIsDebugger(void);|-
10h|ARM9 & ARM7|Récupère chaque bit d'un champ de bits dans un octet.|void swiUnpackBits(uint8 * source, uint32 * destination, PUnpackStruct params);|source est l'adresse du champ de bits, destination est l'adresse de récupération des bits en octet, et params est la structure de paramètres.
11h|ARM9 & ARM7| |void swiDecompressLZSSWram(void * source, void * destination);|
12h|ARM9 & ARM7| |int swiDecompressLZSSVram(void * source, void * destination, uint32 toGetSize, TDecompressionStream * stream);|?
13h|ARM9 & ARM7| |int swiDecompressHuffman(void * source, void * destination, uint32 toGetSize, TDecompressionStream * stream);|?
14h|ARM9 & ARM7| |void swiDecompressRLEWram(void * source, void * destination);|
15h|ARM9 & ARM7| |int swiDecompressRLEVram(void * source, void * destination, uint32 toGetSize, TDecompressionStream * stream);|?
16h|ARM9| |extern void swiDecodeDelta8(void * source, void * destination);|
18h|ARM9| |void swiDecodeDelta16(void * source, void * destination);|
1Ah|ARM7|Récupère un sinus.|uint16 swiGetSineTable(int index);|index est l'entier dont on souhaite calculer le sinus.
1Bh|ARM7|Récupère un pitch.|uint16 swiGetPitchTable(int index);|index est l'index du pitch recherché dans la table.
1Ch|ARM7|Récupère un volume dans la table des volumes.|uint8 swiGetVolumeTable(int index);|index est l'index du volume.
1Dh|ARM9 & ARM7| | |
1Fh|ARM9|Ecriture dans le registre POSTFLG.|-|-
1Fh|ARM7|Ecriture dans le registre HALTCNT.|-|-
Pour utiliser les interruptions softwares qui ne sont pas gérées par la libnds, il faut utiliser du code ARM, mais cela dépasse les limites de ce tutoriel. Heureusement, ces quelques fonctions sont très peu utilisées.
9. Inter Processor Communication (IPC)
10. Mots de la fin
10.1 Remerciements
Je voudrais remercier :
Dr.Vince pour les modifications qu'il a apporté au système de BBCode, notamment sur les tableaux, et pour ses conseils.
Arcadia, pour ses news, toujours aussi amusantes, et qui font franchement plaisir !
Foxy, Noda et simonomis pour les corrections de coquilles
Ceux que j'ai dû oublier ...
10.2 Liens
Téléchargement de devkitpro : http://sourceforge.net/project/showfiles.php?group_id=114505
Téléchargement de la libnds : http://sourceforge.net/project/showfiles.php?group_id=114505&package_id=151608
L'excellente GBATek, de Martin Korth : http://nocash.emubase.de/gbatek.htm
DSTek, de Neimod : http://neimod.com/dstek/
## Partie B, Pratique : ShootMe
Cette seconde partie est un complément à la première, et consiste en une application des notions abordées dans la partie documentation en utilisant bien sûr le langage C. Elle vous montrera, par l'intermédiaire de la création d'un petit jeu, ShootMe, comment utiliser tout ce que je présente plus haut.
Comme son nom l'indique, cet homebrew sera un FPS, avec tout ce qui tourne autour, ce qui permettra d'aborder tout et n'importe quoi.
Attention : comme je ne rédige pas la partie théorique de façon linéaire, les exercices présents dans cette seconde partie sont dans un ordre qui ne suit pas l'ordre de la première partie. Il faut donc parcourir le tout, ou lire chaque passage au fur et à mesure que le besoin s'en fait. Pour ce faire, chaque exercice est numéroté, en rapport avec le passage auquel il se rapporte, et non de manière linéaire.
Bonne lecture, et bons tests !
Exercice 1.0 Une console pour notre jeu
Le but de ce premier exercice est très simple : initialiser une console sur l'écran secondaire, et échanger les 2 écrans, pour obtenir un espace de communication avec l'utilisateur pendant la phase d'initialisation. Tout ça dans une fonction qui nous servira de fonction d'initialisation :
static inline void Initialize(void);
Ensuite, il faudra ajouter un appel à cette fonction dans le main, et afficher le texte suivant : "ShootMe\n-------\n\n".
AIDE : pour échanger les 2 écrans et mettre l'écran principal en haut, il faut utiliser la fonction lcdMainOnTop() ; pour faire l'inverse, il faut utiliser la fonction lcdMainOnBottom(). Ceci est utile, car seul l'écran principal est capable d'afficher de la 3D, par exemple.
Correction 1.0 Une console pour notre jeu
#include <nds.h>
#include <stdio.h>
static inline void Initialize(void);
int main(int argc, char * argv[])
{
Initialize();
iprintf("ShootMe\n-------\n\n");
while (1)
{
swiWaitForVBlank();
}
return 0;
}
static inline void Initialize(void)
{
irqInit();
irqSet(IRQ_VBLANK, 0);
lcdMainOnTop();
videoSetMode(0);
videoSetModeSub(MODE_0_2D | DISPLAY_BG0_ACTIVE);
vramSetBankC(VRAM_C_SUB_BG);
SUB_BG0_CR = BG_MAP_BASE(31);
BG_PALETTE_SUB[255] = RGB15(31, 31, 31);
consoleInitDefault((u16 *) SCREEN_BASE_BLOCK_SUB(31), (u16 *) CHAR_BASE_BLOCK_SUB(0), 16);
}
Version 20070427
par Pitt
## Partie A, Théorie
>> Sommaire
0. Introduction
1. Console, texte
2. Le hardware de la DS
2.1 Présentation
2.2 Connexion sans fil
2.3 Contrôles/sorties
2.4 Mémoire
3. Touches, pad et stylet
3.1 Touches et pad
3.2 Stylet
4. Vidéo 2D
4.1 "Power management"
5. Vidéo 3D
6. Direct Memory Access
7. Timers
8. Interruptions
8.1 Interruptions hardwares
8.2 Interruptions softwares
9. Inter Processor Communication (IPC)
10. Mots de la fin
10.1 Remerciements
10.2 Liens
0. Introduction
Ce tutoriel s'adresse à tous ceux qui souhaitent se lancer dans la programmation sur DS à l'aide, ou pas, de la libnds. Il vise plus à servir de référence qu'à initier, donc une expérience en programmation sur GBA, ou sur DS avec la PAlib constitue un atout non négligeable pour en débuter la lecture. J'essayerai d'être le plus court et concis possible, tout en restant relativement précis.
Comme son nom ne l'indique pas, ce tutoriel n'est pas destiné qu'à ceux qui veulent utiliser la libnds : j'essayerai d'ajouter le plus possible les adresses des registres et les différentes constantes et autres bits, pour permettre à ceux qui voudraient se débrouiller seuls ou seulement comprendre un peu mieux le hardware DS, d'avoir une documentation relativement précise et pas trop barbante ...
Pour le moment, ce tutoriel n'explique pas comment installer devkitpro et libnds, mais c'est tellement simple, et il y a suffisamment de bons tutoriels à ce sujet sur PA et sur le web pour que j'évite de le faire. Une fois ce document terminé, je le rajouterai peut-être, mais pas avant que j'aie mis toutes les choses utiles. Au cas où, vous avez les liens à la fin du tutoriel.
Bonne lecture !
N.B.: Sauf indication contraire, vous pouvez utiliser le template arm9 des exemples de la libnds pour tester les notions abordées dans ce tutoriel.
1. Console, texte
Avant d'entrer dans les détails techniques, il peut être sympathique d'aborder de façon simple le développement sur DS. Cette première partie montre donc brièvement comment afficher du texte grâce à une console par défaut, initialisée par la libnds.
Certaines notions peuvent vous être complètement inconnues, surtout si vous n'avez jamais programmé sur GBA. Mais le but de cette première partie est justement d'afficher un petit quelque chose, pour le plaisir de ceux qui débutent.
Vous pourrez donc revenir à cette partie plus tard, notamment après avoir lu la partie "Vidéo 2D" de ce tutoriel, pour mieux comprendre le pourquoi du comment.
Voici donc un petit Hello world commenté, qui vous permettra d'afficher du texte, même sans vraiment comprendre comment cela fonctionne réellement :
#include <nds.h>
#include <stdio.h>
int main(void)
{
/**
* Il n'est pas nécessaire de comprendre cette première partie,
* sauf si vous avez déjà lu le tutoriel en son ensemble.
* Donc débutant, passez directement à la case 7.
*/
/* 1) Initialisation des interruptions */
irqInit();
irqSet(IRQ_VBLANK, 0);
/* 2) On initialise l'écran supérieur, mais pas
* le tactile, étant donné qu'il nous est inutile. */
videoSetMode(0);
videoSetModeSub(MODE_0_2D | DISPLAY_BG0_ACTIVE);
/* 3) On active la bonne banque de données sur notre écran */
vramSetBankC(VRAM_C_SUB_BG);
/* 4) On configure l'adresse de la carte de notre fond */
SUB_BG0_CR = BG_MAP_BASE(31);
/* 5) La couleur du texte correspond à la couleur 255
* de la palette ; on utilise la macro RGB() */
BG_PALETTE_SUB[255] = RGB15(31, 31, 31);
/* 6) On initialise notre console */
consoleInitDefault((u16 *) SCREEN_BASE_BLOCK_SUB(31), (u16 *) CHAR_BASE_BLOCK_SUB(0), 16);
/* 7) On affiche du texte avec la fonction iprintf,
* qui prend comme unique argument le texte à afficher */
iprintf("Hello world !\n");
/* 8) Une boucle infinie, pour éviter de freezer,
* ça serait dommage ... */
while (1);
return 0;
}
Cette première partie n'est finalement qu'un aperçu pour les plus pressés, et elle n'apprend pas grand chose ... La troisième partie traite des différentes entrées basiques de la DS. Mais avant cela, il faut bien parler du hardware de la DS.
2. Le hardware de la DS
2.1 Présentation
La Nintendo Dual Screen, ou DS, est une console portable. Je précise au cas où vous ne seriez pas au courant ... Développée par Nintendo, elle est dotée de 2 processeurs, de la même famille, un ARM946E-S, cadencé à 67 MHz, et un ARM7TDMI, cadencé à 33 MHz. Pour le moment, nous n'utiliserons "que" l'ARM9, pour la simple et bonne raison que c'est celui qui nous permet d'utiliser les fonctions principales de la DS. Mais par la suite, nous utiliserons le second processeur.
2.2 Connexion sans fil
La DS gère la communication sans fil de 2 manières : en utilisant la norme IEEE 802.11 pour la connexion à internet, et un format propriétaire Nintendo, le NiFi, pour communiquer entre DS.
La première méthode peut être gérée via la libwifi, mais il n'y a encore aucune librairie fonctionnelle permettant d'utiliser le NiFi.
2.3 Contrôles/sorties
La DS est dotée de 2 écrans, dont un tactile. Ils ont tous les deux une résolution de 256x192 pixels, un pitch de 0,24 mm et ils gèrent 262 000 couleurs.
En plus de l'écran tactile, la DS possède de nombreux contrôles : une croix multidirectionnelle, et 8 boutons X, Y, A, B, L, R, Start et Select. Elle dispose également d'un micro intégré, en dessous de l'écran tactile pour les DS, et entre les deux écrans pour la DS Lite.
Enfin, elle dispose de 2 haut-parleurs stéréo.
2.4 Mémoire
La DS est dotée de 4 Mo de mémoire RAM. De plus, l'ARM7 peut accéder à une mémoire de 64 ko, et la console possède également une zone de mémoire partagée par l'ARM9 et l'ARM7, de 32 ko. Enfin, une mémoire de 656 ko est réservée à la vidéo, c'est-à-dire à l'unité graphique de la DS.
Le tableau suivant montre les types de données utilisés par la DS, et leurs equivalents utilisés par GCC :
Nom|Taille en bits
{colsp=2}Types génériques
Nibble|4
Octet (byte)|8
Demi-mot (halfword ou hword)|16
Mot (word)|32
Double-mot (doubleword ou dword)|64
{colsp=2}Types utilisés par GCC
char|8
short|16
long|32
long long|64
float|32
double|64
Voici également un tableau des différents types déclarés par la libnds :
Non-signé|Signé|Volatile non-signé|Volatile signé|Taille en bits|Abbréviations
bool|-|-|-|8|-
uint8|int8|vuint8|vint8|8|u8, s8, vu8, vs8
uint16|int16|vuint16|vint16|16|u16, s16, vu16, vs16
uint32|int32|vuint32|vint32|32|u32, s32, vu32, vs32
uint64|int64|vuint64|vint64|64|u64, s64, vu64, vs64
-|float32|-|vfloat32|32|-
-|float64|-|vfloat64|64|-
De plus, la DS est Little Endian, donc les octets qui composent chaque mot sont inversés. Par exemple le double-mot 0xBADC0FEE sera stocké comme suit :
Octet|3|2|1|0
Valeur|BA|DC|OF|EE
3. Touches, pad et stylet
La gestion des touches, du pad et du stylet est extrêmement simple sur DS. Cette troisième partie passe rapidement en revue les registres, et les fonctions de la libnds, servant à l'utilisation des entrées de la DS, du moins au niveau des doigts ...
3.1 Touches et pad
La DS stocke l'état actuel des différentes touches dans le registre REG_KEYINPUT, registre 16 bits situé à l'adresse 0x04000130.
Bit|13|12|11|10|9|8|7|6|5|4|3|2|1|0
Nom|KEY_LID|KEY_TOUCH|KEY_Y|KEY_X|KEY_L|KEY_R|KEY_ DOWN|KEY_UP|KEY_LEFT|KEY_RIGHT|KEY_START|KEY_SELEC T|KEY_B|KEY_A
Processeur|ARM7|ARM7|ARM7|ARM7|Les 2|Les 2|Les 2|Les 2|Les 2|Les 2|Les 2|Les 2|Les 2|Les 2
Représente|Diode|Ecran tactile|Bouton Y|Bouton X|Bouton L|Bouton R|Bas|Haut|Gauche|Droite|Start|Select|Bouton B|Bouton A
Il suffit donc de lire le contenu de ce registre, et d'utiliser des masques de bits pour détecter quelles touches sont pressées. Certains des bits ne sont actualisés que pour l'ARM7, mais cela importe peu quand on programme à l'aide de la libnds, car celle-ci fait passer le contenu de REG_KEYINPUT à l'ARM9 via l'IPC (voir plus loin).
La libnds propose plusieurs déclarations concernant la lecture des touches :
Une fonction permettant de récupérer le contenu du registre REG_KEYINPUT dans un buffer.
void scanKeys();
2 fonctions permettant de savoir, une fois le contenu de REG_KEYINPUT mis en buffer, quelles touches sont respectivement :
- maintenues appuyées
- pressées
- relevées
depuis le dernier appel à scanKeys().
uint32 keysHeld(void);
uint32 keysDown(void);
uint32 keysUp(void);
/!\ Si le joueur reste appuyé sur un bouton, keysDown() renverra 0 pour ce bouton. C'est alors keysHeld() qui prendra le relais.
il ne faut donc pas confondre ces 2 fonctions, et distinguer la différence entre une touche relevée, pressée ou maintenue.
2 fonctions permettant respectivement de détecter la répétition d'un appui sur une touche, et de paramètrer la répétition des touches.
uint32 keysDownRepeat(void);
/**
* setDelay est le nombre d'appels à scanKeys() avant que les touches commencent à se répéter.
* setRepeat est le nombre d'appels à scanKeys() avant que les touches ne se répètent.
*/
void keysSetRepeat( u8 setDelay, u8 setRepeat );
Une énumération représentant la valeur des différentes touches.
typedef enum KEYPAD_BITS {
/* Touches */
KEY_A = BIT(0),
KEY_B = BIT(1),
KEY_SELECT = BIT(2),
KEY_START = BIT(3),
KEY_RIGHT = BIT(4),
KEY_LEFT = BIT(5),
KEY_UP = BIT(6),
KEY_DOWN = BIT(7),
KEY_R = BIT(8),
KEY_L = BIT(9),
KEY_X = BIT(10),
KEY_Y = BIT(11),
/* Ecran tactile */
KEY_TOUCH = BIT(12),
/* Etat de la diode */
KEY_LID = BIT(13)
} KEYPAD_BITS;
Un usage fréquent de ces fonctions est présenté ci-dessous :
scanKeys();
if (keysDown() & KEY_A)
{
/* si le bouton A est pressé ...*/
}
3.2 Stylet
Pour savoir si le stylet est appuyé sur l'écran, il suffit d'effectuer un scanKeys(), puis de tester keysDown() avec le masque de bit KEY_TOUCH.
Pour connaitre l'endroit où le stylet est appuyé, il faut utiliser la fonction suivante, qui retourne une structure touchPosition. Cette structure contient 6 champs, qui sont expliqué plus bas.
touchPosition touchReadXY();
/* La structure touchPosition */
typedef struct touchPosition {
int16 x; /* Coordonnée X de l'endroit où l'écran est touché */
int16 y; /* Coordonnée Y de l'endroit où l'écran est touché */
int16 px; /* Coordonnée X du pixel à l'endroit où l'écran est touché */
int16 py; /* Coordonnée Y du pixel à l'endroit où l'écran est touché */
int16 z1; /* FIXME : profondeur de l'appui ? */
int16 z2; /* FIXME : profondeur de l'appui ? */
} touchPosition;
Cette structure est passée à l'ARM9 par l'ARM7 via une zone commune aux 2 processeurs, qui permet d'échanger des informations entre eux, l'IPC. Cette zone est expliquée plus en détail dans la partie 9, et ce sera par ailleurs l'occasion d'expliquer plus en détail les registres qui concernent la récupération des coordonnées du stylet, de la pression des touches X et Y, et de l'écran tactile.
4. Vidéo 2D
La DS est dotée d'un hardware relativement bien fourni concernant la 2D : il existe de multiples façons d'accéder aux écrans, et d'y afficher ce que l'on désire.
Il faudra bien comprendre dans cet partie la différence entre l'écran principal, généralement (mais pas tout le temps) utilisé en bas, et l'écran secondaire. Pourquoi cette distinction ? Parce qu'on peut échanger la place des 2 écrans, ce qui peut fausser toutes vos fonctions d'affichage si vous n'y pensez pas. De plus, la section suivante s'intéresse au hardware 3D, qui n'est utilisable QUE sur l'écran principal.
4.1 "Power management"
Tout d'abord, l'unité LCD est régie par un registre de gestion de l'alimentation, qui permet d'activer, ou non, les 2 écrans, mais aussi de déterminer quelles opérations sont activées.
C'est le registre 16bits REG_POWERCNT, situé à l'adresse 0x04000304.
Le tableau suivant récapitule les différents bits activables de REG_POWERCNT :
Bit|Define de la libnds|Action
0|POWER_LCD|Active les 2 écrans LCD
1|POWER_2D_A|Active la 2D sur l'écran principal
2|POWER_MATRIX|Active les matrices 3D
3|POWER_3D_CORE|Active la 3D sur l'écran principal
9|POWER_2D_B|Active la 2D sur l'écran secondaire
15|POWER_SWAP_LCDS|Echange les 2 écrans
Afin de simplifier la vie des codeurs, la libnds contient 2 macros supplémentaires :
- POWER_ALL_2D, qui active toute la 2D (écrans, 2D sur chaque écran)
- POWER_ALL, qui active tout (écrans, 2D sur chaque écran, 3D)
La libnds fournit plusieurs fonctions concernant le "power management" :
Une fonction qui ajoute les paramètres qu'on lui donne
static inline void powerON(int on);
Une fonction qui n'active QUE les paramètres qu'on lui donne
static inline void powerSET(int on);
Une fonction qui désactive les paramètres qu'on lui donne
static inline void powerOFF(int off);
3 fonctions de gestion de la position des écrans
/* - Echange les 2 écrans */
static inline void lcdSwap(void);
/* - Mettre l'écran principal en haut */
static inline void lcdMainOnTop(void);
/* - Mettre l'écran principal en bas */
static inline void lcdMainOnBottom(void);
Deux utilisations typiques sont présentées ci-dessous :
/* - utilisation de la 2D uniquement */
powerON(POWER_ALL_2D);
/* - utilisation de la 2D et de la 3D */
powerON(POWER_ALL);
5. Vidéo 3D
6. Direct Memory Access
Pour transférer de manière rapide, on peut utiliser une technique spéciale, appelée Direct Memory Access, ou DMA. Comme son nom ne l'indique qu'à moitié, cette méthode demande au processeur de stopper l'exécution du programme et de transférer directement des données en mémoire. Cela peut avoir des avantages considérables, surtout lors de l'utilisation de modes graphiques bitmaps, ou encore pour transférer des sons vers les hauts-parleurs. Mais en contre partie, le DMA stoppe l'exécution du processeur, ce qui peut être vu comme un inconvénient.
La DS possède 4 canaux de transfert DMA, qui sont utilisables grâce aux registres DMA_CR(x), DMA_SRC(x) et DMA_DEST(x), où x est le numéro de canal compris entre 0 et 3. C'est 4 canaux ne diffèrent que par l'usage qu'on leur réserve généralement :
- canal 0 : le plus rapide, surtout utilisé pour les transferts critiques.
- canaux 1 et 2 : utilisés pour le transfert du son.
- canal 3 : les autres transferts.
Ces indications sont bien entendues théoriques ; le canal 0 peut très bien être utilisé pour transfèrer du son, et on peut très bien copier une structure avec le canal 1.
Le premier registre, DMA_CR(x), permet le contrôle du transfert DMA ; le tableau suivant récapitule les paramètres qui peuvent être utilisés pour le contrôle d'un transfert, via le registre DMA_CR(x) qui est un registre 32 bits situé à l'adresse :
- 0x040000B0 pour le canal 0.
- 0x040000BC pour le canal 1.
- 0x040000C8 pour le canal 2.
- 0x040000D4 pour le canal 3.
Paramètre|Bit|Processeur|Action
{colsp=2}Activation du transfert
DMA_ENABLE|31|ARM7 & ARM9|Active le transfert DMA sur ce canal.
DMA_IRQ_REQ|30|ARM7 & ARM9|Active la génération d'une interruption à la fin du transfert.
DMA_START_NOW|-|ARM7 & ARM9|Commence le transfert DMA immédiatement.
DMA_START_CARD|-|ARM7 & ARM9|
DMA_START_VBL|27|ARM7 & ARM9|Commence le transfert DMA au prochain VBL.
DMA_START_HBL|28|ARM9|Commence le transfert DMA au prochain HBL.
DMA_START_FIFO|-|ARM9|
DMA_DISP_FIFO|-|ARM9|
{colsp=2}Tailles de transfert
DMA_16_BIT|-|ARM7 & ARM9|Transfert de demi-mots (halfwords = 16 bits).
DMA_32_BIT|26|ARM7 & ARM9|Transfert de mots (words = 32 bits).
{colsp=2}Types de transfert
DMA_REPEAT|25|ARM7 & ARM9|
DMA_SRC_INC|-|ARM7 & ARM9|Récupère les données à copier de manière ascendante.
DMA_SRC_DEC|23|ARM7 & ARM9|Récupère les données à copier de manière descendante.
DMA_SRC_FIX|24|ARM7 & ARM9|Copie toujours le même demi-mot/mot.
DMA_DST_INC|-|ARM7 & ARM9|Copie les données de manière ascendante.
DMA_DST_DEC|21|ARM7 & ARM9|Copie les données de manière descendante.
DMA_DST_FIX|22|ARM7 & ARM9|Copie toujours au même endroit.
DMA_DST_RESET|-|ARM7 & ARM9|
{colsp=2}Transferts prédéfinis
DMA_COPY_WORDS|-|Transfert immédiat de mots.
DMA_COPY_HALFWORDS|-|Transfert immédiat de demi-mots.
DMA_FIFO|-|
Le bit 31 (= DMA_BUSY) du registre DMA_CR(x) indique, une fois le transfert commencé, si le canal est occupé ou non. La libnds fournit également la fonction qui suit, et qui permet de savoir si le canal DMA choisi est libre, ou pas :
static inline int dmaBusy(uint8 channel);
Le second registre, DMA_SRC(x), contient l'adresse des données à copier.
C'est également un registre 32 bits, situé à l'adresse :
- 0x040000B4 pour le canal 0.
- 0x040000C0 pour le canal 1.
- 0x040000CC pour le canal 2.
- 0x040000D8 pour le canal 3.
Le troisième registre, DMA_DEST(x), contient l'adresse à laquelle les données seront transférées.
Celui-ci est encore une fois un registre 32 bits, situé à l'adresse :
- 0x040000B8 pour le canal 0.
- 0x040000C4 pour le canal 1.
- 0x040000D0 pour le canal 2.
- 0x040000DC pour le canal 3.
La libnds fournit plusieurs fonctions de transferts prédéfinies :
Transferts synchrones
/* - de mots */
static inline dmaCopyWords(uint8 channel, const void* src, void* dest, uint32 size);
/* - de demi-mots */
static inline void dmaCopyHalfWords(uint8 channel, const void* src, void* dest, uint32 size);
Transferts Asynchrones
/* - de mots */
static inline void dmaCopyWordsAsynch(uint8 channel, const void* src, void* dest, uint32 size);
/* - de demi-mots */
static inline void dmaCopyHalfWordsAsynch(uint8 channel, const void* src, void* dest, uint32 size);
Transferts "à la memcopy", utilisant le canal 3
/* - synchrone */
static inline void dmaCopy(const void * source, void * dest, uint32 size);
/* - asynchrone */
static inline void dmaCopyAsynch(const void * source, void * dest, uint32 size);
Sinon, une utilisation typique du DMA se déroule comme suit :
/* Version synchrone */
DMA_SRC(canal) = adresse_source;
DMA_DEST(canal) = adresse_destination;
DMA_CR(canal) = parametres | taille;
while (dmaBusy(canal));
/* Version asynchrone */
DMA_SRC(canal) = adresse_source;
DMA_DEST(canal) = adresse_destination;
DMA_CR(canal) = parametres | taille;
7. Timers
Un timer est un registre compteur, qui joue le rôle d'horloge. Il permet par exemple d'attendre un certain temps, ou d'effectuer une action à intervalles réguliers. La DS possède 4 timers, numérotés de 0 à 3, qui sont tous les quatre cadencés à 33.514 MHz. L'utilisation de ces derniers est très simple, et s'effectue grâce à 2 registres.
Le premier, TIMER_CR(x), où x est le numéro du timer, permet de gérer, comme pour les transferts DMA, la configuration et la mise en place du timer. Le tableau qui suit montre les différents paramètres que l'on peut sélectionner. Le registre TIMER_CR(x) est un registre 16 bits, qui se situe à l'adresse :
- 0x04000102 pour le timer 0.
- 0x04000106 pour le timer 1.
- 0x0400010A pour le timer 2.
- 0x0400010E pour le timer 3.
Paramètre|Bit|Action
TIMER_ENABLE|7|Active le timer.
TIMER_IRQ_REQ|6|Active la génération d'une interruption lors du débordement du timer.
TIMER_CASCADE|2|Active la mise en cascade des timers, c'est-à-dire que le timer ne s'active que lorsque le timer précédent déborde. Ne peut être utilisé sur le timer 0.
TIMER_DIV_1|0-1|Cadence le timer à 33.514 MHz.
TIMER_DIV_64|0-1|Cadence le timer à (33.514 / 64) MHz.
TIMER_DIV_256|0-1|Cadence le timer à (33.514 / 256) MHz.
TIMER_DIV_1024|0-1|Cadence le timer à (33.514 / 1024) MHz.
Le second registre, TIMER_DATA(x) possède un comportement un peu plus compliqué. En effet, il permet en écriture de sélectionner la fréquence du compteur, grâce aux macros présentées dans le tableau suivant, qui sont utilisées suivant la division de la cadence du timer. Mais en lecture, il renvoit la valeur actuelle du compteur.
Ce registre de 16 bits se situe à l'adresse :
- 0x04000100 pour le timer 0.
- 0x04000104 pour le timer 1.
- 0x04000108 pour le timer 2.
- 0x0400010C pour le timer 3.
Paramètre de division du timer|Macro pour le choix de la fréquence x
TIMER_DIV_1|TIMER_FREQ(x)
TIMER_DIV_64|TIMER_FREQ_64(x)
TIMER_DIV_256|TIMER_FREQ_256(x)
TIMER_DIV_1024|TIMER_FREQ_1024(x)
La libnds ne fournit aucune fonction de gestion de timers, mais leur utilisation est très simple, et ce fait comme suit :
/* En utilisant la bonne macro pour calculer la fréquence */
TIMER_CR(numero) = parametres;
TIMER_DATA(numero) = frequence;
8. Interruptions
8.1 Interruptions hardwares
Le processeur ARM9 utilisé jusqu'ici, tout comme l'ARM 7 d'ailleurs, lit les instructions du programme courant les unes après les autres, et les exécute. Il est relié à différents périphériques, comme par exemple les touches ou les hauts-parleurs.
Pour échanger des informations entre les périphériques, on peut utiliser 2 méthodes :
- la première, appelée polling consiste à attendre dans une boucle infinie qu'une condition soit remplie, comme par exemple la pression du bouton R. Cette méthode a ses avantages, mais aussi ses inconvénients, vus qu'on ne peut pas vraiment effectuer de longues tâches pendant la période d'attente.
- la seconde consiste à utiliser un système d'interruptions. Une interruption, au sens électronique du terme, est un simple fil, qui relie un périphérique au processeur. Cet entrée conserve toujours la même valeur, soit 0 ou 1. Mais lorsque le périphérique a besoin d'informer le programme d'un évènement, il change la valeur de l'entrée. Le processeur interrompt alors l'exécution du programme, et exécute la routine vers laquelle il est configuré pour pointer lors de l'interruption. Une fois cette routine exécutée, l'entrée reprend sa valeur initiale, et l'exécution du programme continue là où elle s'était arrêtée.
Tout comme le polling, cette technique possède des avantages et des inconvénients, mais les interruptions possèdent un panel très varié : touches pressées, évènement Wifi, fin de transfert DMA ...
La DS possède 23 interruptions ; le tableau suivant les liste, indique le processeur qui y a accès, et donne une brève description de ces dernières.
Nom|Bit|Processeur|Description
IRQ_VBLANK|0|ARM9 & ARM7|Générée lors de la période de VBlank.
IRQ_HBLANK|1|ARM9 & ARM7|Générée lors de la période de HBlank.
IRQ_VCOUNT|2|ARM9 & ARM7|Générée lorsque la valeur du VCOUNT correspond à la valeur configurée dans DISPSTAT.
IRQ_TIMER0|3|ARM9 & ARM7|Générée lors du débordement du timer 0.
IRQ_TIMER1|4|ARM9 & ARM7|Générée lors du débordement du timer 1.
IRQ_TIMER2|5|ARM9 & ARM7|Générée lors du débordement du timer 2.
IRQ_TIMER3|6|ARM9 & ARM7|Générée lors du débordement du timer 3.
IRQ_NETWORK|7|ARM7|
IRQ_DMA0|8|ARM9 & ARM7|Générée lors de la fin d'un transfert DMA sur le canal 0.
IRQ_DMA1|9|ARM9 & ARM7|Générée lors de la fin d'un transfert DMA sur le canal 1.
IRQ_DMA2|10|ARM9 & ARM7|Générée lors de la fin d'un transfert DMA sur le canal 2.
IRQ_DMA3|11|ARM9 & ARM7|Générée lors de la fin d'un transfert DMA sur le canal 3.
IRQ_KEYS|12|ARM9 & ARM7|Générée lorsque la valeur des touches pressées est égale à la valeur stockée dans REG_KEYCNT.
IRQ_CART|13|ARM9 & ARM7|Générée par la cartouche GBA.
IRQ_IPC_SYNC|16|ARM9 & ARM7|Générée lors de la synchronisation de l'IPC.
IRQ_FIFO_EMPTY|17|ARM9 & ARM7|Générée lors de l'envoi d'un FIFO vide.
IRQ_FIFO_NOT_EMPTY|18|ARM9 & ARM7|Générée lors de la réception d'un FIFO vide.
IRQ_CARD|19|ARM9 & ARM7|Générée à la fin d'un transfert de données depuis la carte.
IRQ_CARD_LINE|20|ARM9 & ARM7|
IRQ_GEOMETRY_FIFO|21|ARM9|Interruption géométrie FIFO.
IRQ_LID|22|ARM7|
IRQ_SPI|23|ARM7|Interruption du bus SPI.
IRQ_WIFI|24|ARM7|Générée lorsqu'un évènement Wifi se produit.
IRQ_ALL|~0|ARM9 & ARM7|L'ensemble des interruptions.
On peut activer chaque interruption en activant le bit correspondant dans le registre REG_IE, registre 32 bits situé à l'adresse 0x04000210.
De plus, il peut être utile de désactiver momentanément toutes les interruptions. Pour cela, le bit 0 du registre REG_IME, registre 16 bits situé à l'adresse 0x04000208, permet de désactiver toutes les interruptions lorsqu'il est mis à 0, que le bit de l'interruption soit activé ou non dans le registre REG_IE.
La libnds propose plusieurs fonctions pour gérer les interruptions :
une fonction d'initialisation globale
void irqInit();
2 fonctions permettant respectivement d'associer une fonction à une interruption, ou de supprimer la fonction associée à cette interruption
/* Associer : */
void irqSet(IRQ_MASK irq, VoidFunctionPointer handler);
/* Effacer : */
void irqClear(IRQ_MASK irq);
2 fonctions, permettant respectivement d'activer ou de désactiver une interruption
/* Activer : */
void irqEnable(IRQ_MASK irq);
/* Désactiver : */
void irqDisable(IRQ_MASK irq);
une fonction permettant d'utiliser un "interrupt handler" différent de celui utilisé par la libnds
void irqInitHandler(VoidFunctionPointer handler);
8.2 Interruptions softwares
Dans la partie précédente, j'ai usé et abusé du terme d'interruption. en réalité, c'est un abus de langage, et les interruptions précédentes étaient en réalité des interruptions hardwares. Il existe un autre type d'interruptions, qui fonctionnent comme les interruptions hardware, mais qui sont déclenchées par le programme lui-même. On appelle ces interruptions interruptions softwares (abréviation : SWI, pour SoftWare Interrupt).
Pour utiliser une interruption software, il suffit d'utiliser l'instruction ARM swi. Même s'il est possible d'utiliser cette instruction en C via asm(), on préfèrera utiliser les fonctions libnds correspondantes, lorsqu'elles existent. La DS possède 25 interruptions softwares, que le tableau suivant récapitule, tout en leur associant les fonctions libnds qui leur correspondent.
Valeur|Processeur|Fonction|Fonction libnds|Explication des paramètres
00h|ARM9 & ARM7|Effectue un reset software de la DS.|void swiSoftReset(void);|-
03h|ARM9 & ARM7|Attends un certain temps.|void swiDelay(uint32 duration);|duration est la durée à attendre.
04h|ARM9 & ARM7|Attends une interruption hardware.|void swiIntrWait(int waitForSet, uint32 flags);|Si waitForSet vaut 0, retourne si l'interruption a déjà eu lieu. Si waitForSet vaut 1, attend que l'interruption ait lieu. flags est l'interruption à attendre.
05h|ARM9 & ARM7|Attends l'interruption VBL.|void swiWaitForVBlank(void);|-
06h|ARM9 & ARM7|Stoppe le processeur|void swiHalt(void);|-
07h|ARM7|Mise en veille|void swiSleep(void);|-
08h|ARM7|change le registre SOUNDBIAS|void swiChangeSoundBias(int enabled, int delay);|enabled est le niveau de BIAS, delay est le délai (ex.: 8 sur GBA).
09h|ARM9 & ARM7|Division et modulo signé|int swiDivide(int numerator, int divisor);| numerator est le numérateur, et divisor le diviseur. Retourne le quotient.
Idem|Idem|Idem|int swiRemainder(int numerator, int divisor);| numerator est le numérateur, et divisor le diviseur. Retourne le modulo.
0Bh|ARM9 & ARM7|Copie|void swiCopy(const void * source, void * dest, int flags);|Les bits 0-20 de flags correspondent à la taille du transfert, le bit 24 au type de transfert (COPY_MODE_COPY ou COPY_MODE_FILL, copie ou remplissage) et le bit 26 à la taille du transfert(COPY_MODE_HWORD ou COPY_MODE_WORD, demi-mot ou mot).
0Ch|ARM9 & ARM7|Copie rapide|void swiFastCopy(const void * source, void * dest, int flags);|Les bits 0-20 de flags correspondent à la taille du transfert et le bit 24 au type de transfert (COPY_MODE_COPY ou COPY_MODE_FILL, copie ou remplissage).
0Dh|ARM9 & ARM7|Racine carrée|int swiSqrt(int value);|value est le nombre dont on cherche la racine carrée.
0Eh|ARM9 & ARM7|Retourne le CRC-16|uint16 swiCRC16(uint16 crc, void * data, uint32 size);|crc est le CRC-16 initial, data est un pointeur vers les données, et size est la taille des données en octets.
0Fh|ARM9 & ARM7|Retourne 1 si le programmer tourne sur debuggeur hardware.|int swiIsDebugger(void);|-
10h|ARM9 & ARM7|Récupère chaque bit d'un champ de bits dans un octet.|void swiUnpackBits(uint8 * source, uint32 * destination, PUnpackStruct params);|source est l'adresse du champ de bits, destination est l'adresse de récupération des bits en octet, et params est la structure de paramètres.
11h|ARM9 & ARM7| |void swiDecompressLZSSWram(void * source, void * destination);|
12h|ARM9 & ARM7| |int swiDecompressLZSSVram(void * source, void * destination, uint32 toGetSize, TDecompressionStream * stream);|?
13h|ARM9 & ARM7| |int swiDecompressHuffman(void * source, void * destination, uint32 toGetSize, TDecompressionStream * stream);|?
14h|ARM9 & ARM7| |void swiDecompressRLEWram(void * source, void * destination);|
15h|ARM9 & ARM7| |int swiDecompressRLEVram(void * source, void * destination, uint32 toGetSize, TDecompressionStream * stream);|?
16h|ARM9| |extern void swiDecodeDelta8(void * source, void * destination);|
18h|ARM9| |void swiDecodeDelta16(void * source, void * destination);|
1Ah|ARM7|Récupère un sinus.|uint16 swiGetSineTable(int index);|index est l'entier dont on souhaite calculer le sinus.
1Bh|ARM7|Récupère un pitch.|uint16 swiGetPitchTable(int index);|index est l'index du pitch recherché dans la table.
1Ch|ARM7|Récupère un volume dans la table des volumes.|uint8 swiGetVolumeTable(int index);|index est l'index du volume.
1Dh|ARM9 & ARM7| | |
1Fh|ARM9|Ecriture dans le registre POSTFLG.|-|-
1Fh|ARM7|Ecriture dans le registre HALTCNT.|-|-
Pour utiliser les interruptions softwares qui ne sont pas gérées par la libnds, il faut utiliser du code ARM, mais cela dépasse les limites de ce tutoriel. Heureusement, ces quelques fonctions sont très peu utilisées.
9. Inter Processor Communication (IPC)
10. Mots de la fin
10.1 Remerciements
Je voudrais remercier :
Dr.Vince pour les modifications qu'il a apporté au système de BBCode, notamment sur les tableaux, et pour ses conseils.
Arcadia, pour ses news, toujours aussi amusantes, et qui font franchement plaisir !
Foxy, Noda et simonomis pour les corrections de coquilles
Ceux que j'ai dû oublier ...
10.2 Liens
Téléchargement de devkitpro : http://sourceforge.net/project/showfiles.php?group_id=114505
Téléchargement de la libnds : http://sourceforge.net/project/showfiles.php?group_id=114505&package_id=151608
L'excellente GBATek, de Martin Korth : http://nocash.emubase.de/gbatek.htm
DSTek, de Neimod : http://neimod.com/dstek/
## Partie B, Pratique : ShootMe
Cette seconde partie est un complément à la première, et consiste en une application des notions abordées dans la partie documentation en utilisant bien sûr le langage C. Elle vous montrera, par l'intermédiaire de la création d'un petit jeu, ShootMe, comment utiliser tout ce que je présente plus haut.
Comme son nom l'indique, cet homebrew sera un FPS, avec tout ce qui tourne autour, ce qui permettra d'aborder tout et n'importe quoi.
Attention : comme je ne rédige pas la partie théorique de façon linéaire, les exercices présents dans cette seconde partie sont dans un ordre qui ne suit pas l'ordre de la première partie. Il faut donc parcourir le tout, ou lire chaque passage au fur et à mesure que le besoin s'en fait. Pour ce faire, chaque exercice est numéroté, en rapport avec le passage auquel il se rapporte, et non de manière linéaire.
Bonne lecture, et bons tests !
Exercice 1.0 Une console pour notre jeu
Le but de ce premier exercice est très simple : initialiser une console sur l'écran secondaire, et échanger les 2 écrans, pour obtenir un espace de communication avec l'utilisateur pendant la phase d'initialisation. Tout ça dans une fonction qui nous servira de fonction d'initialisation :
static inline void Initialize(void);
Ensuite, il faudra ajouter un appel à cette fonction dans le main, et afficher le texte suivant : "ShootMe\n-------\n\n".
AIDE : pour échanger les 2 écrans et mettre l'écran principal en haut, il faut utiliser la fonction lcdMainOnTop() ; pour faire l'inverse, il faut utiliser la fonction lcdMainOnBottom(). Ceci est utile, car seul l'écran principal est capable d'afficher de la 3D, par exemple.
Correction 1.0 Une console pour notre jeu
#include <nds.h>
#include <stdio.h>
static inline void Initialize(void);
int main(int argc, char * argv[])
{
Initialize();
iprintf("ShootMe\n-------\n\n");
while (1)
{
swiWaitForVBlank();
}
return 0;
}
static inline void Initialize(void)
{
irqInit();
irqSet(IRQ_VBLANK, 0);
lcdMainOnTop();
videoSetMode(0);
videoSetModeSub(MODE_0_2D | DISPLAY_BG0_ACTIVE);
vramSetBankC(VRAM_C_SUB_BG);
SUB_BG0_CR = BG_MAP_BASE(31);
BG_PALETTE_SUB[255] = RGB15(31, 31, 31);
consoleInitDefault((u16 *) SCREEN_BASE_BLOCK_SUB(31), (u16 *) CHAR_BASE_BLOCK_SUB(0), 16);
}