Nous créons un jeu de plateforme portable sur le microcontrôleur Cortex M0 +


Présentation


(Des liens vers le code source et le projet KiCAD sont fournis à la fin de l'article.)

Bien que nous soyons nés à l'ère 8 bits, notre premier ordinateur était l'Amiga 500. Il s'agit d'une excellente machine 16 bits avec des graphismes et un son incroyables, ce qui la rend idéale pour les jeux. La plateforme est devenue un genre de jeu très populaire sur cet ordinateur. Beaucoup d'entre eux étaient très colorés et avaient un défilement de parallaxe très lisse. Cela a été rendu possible grâce à des programmeurs talentueux qui ont ingénieusement utilisé des coprocesseurs Amiga pour augmenter le nombre de couleurs d'écran. Jetez un œil à LionHeart par exemple!


Cœur de Lion sur Amiga. Cette image statique ne transmet pas la beauté des graphiques.

Depuis les années 90, l'électronique a beaucoup changé et maintenant il existe de nombreux petits microcontrôleurs qui vous permettent de créer des choses incroyables.

Nous avons toujours aimé les jeux de plate-forme et aujourd'hui, pour seulement quelques dollars, vous pouvez acheter Raspberry Zero, installer Linux et «assez facile» écrire un jeu de plate-forme coloré.

Mais cette tâche n'est pas pour nous - nous ne voulons pas tirer sur des moineaux avec un canon!

Nous voulons utiliser des microcontrôleurs avec une mémoire limitée, et non un système puissant sur une puce avec un GPU intégré! En d'autres termes, nous voulons des difficultés!

Soit dit en passant, sur les possibilités de la vidéo: certaines personnes parviennent à extraire tous les jus du microcontrôleur AVR dans leurs projets (par exemple, dans le projet Uzebox ou Craft du développeur lft). Cependant, pour y parvenir, les microcontrôleurs AVR nous obligent à écrire en assembleur, et même si certains jeux sont très bons, vous rencontrerez de sérieuses limitations qui ne vous permettent pas de créer un jeu en style 16 bits.

Par conséquent, nous avons décidé d'utiliser un microcontrôleur / carte plus équilibré, ce qui nous permet d'écrire du code complètement en C.

Il n'est pas aussi puissant que Arduino Due, mais pas aussi faible que Arduino Uno. Fait intéressant, «dû» signifie «deux» et «Uno» signifie «un». Microsoft nous a appris à compter correctement (1, 2, 3, 95, 98, ME, 2000, XP, Vista, 7, 8, 10), et Arduino a également suivi cette voie! Nous utiliserons l'Arduino Zero, qui se situe entre 1 et 2!

Oui, selon Arduino, 1 <0 <2.

En particulier, nous ne nous intéressons pas à la carte elle-même, mais à sa série de processeurs. L'Arduino Zero possède un microcontrôleur de la série ATSAMD21 avec Cortex M0 + (48 MHz), 256 Ko de mémoire flash et 32 ​​Ko de RAM.

Bien que le Cortex M0 + à 48 MHz surpasse considérablement les performances de l'ancien MC68000 à 7 MHz, l'Amiga 500 avait 512 Ko de RAM, des sprites matériels, une double carte de jeu intégrée, Blitter (un moteur de transfert de blocs d'images basé sur DMA avec un système intégré de reconnaissance des collisions avec précision au pixel près). et la transparence) et Copper (un coprocesseur raster qui vous permet d'effectuer des opérations avec des registres basés sur la position de balayage pour créer de nombreux très beaux effets). SAMD21 n'a pas tout ce matériel (à l'exception d'un matériel assez simple par rapport à Blitter DMA), donc beaucoup seront rendus par programmation.

Nous voulons atteindre les paramètres suivants:

  • Résolution 160 x 128 pixels sur un écran SPI de 1,8 pouces.
  • Graphiques avec 16 bits par pixel;
  • La fréquence d'images la plus élevée. Au moins 25 fps à 12 MHz SPI, ou 40 fps à 24 MHz;
  • double terrain de jeu avec défilement parallaxe;
  • tout est écrit en C. Pas de code assembleur;
  • Reconnaissance au pixel près des collisions;
  • superposition d'écran.

Il semble que la réalisation de ces objectifs soit assez difficile. Ça l'est, surtout si on refuse le code sur asm!

Par exemple, avec une couleur 16 bits, une taille d'écran de 160 × 128 pixels nécessitera 40 Ko pour le tampon d'écran, mais nous n'avons que 32 Ko de RAM! Et nous avons encore besoin du défilement de parallaxe sur un double terrain de jeu et bien plus encore, avec une fréquence d'au moins 25/40 ips!

Mais rien n'est impossible pour nous, non?

Nous utilisons des astuces et des fonctions intégrées de ATSAMD21! En tant que "matériel", nous prenons uChip , qui peut être acheté dans le magasin Itaca .


uChip: le cœur de notre projet!

Il a les mêmes caractéristiques que l'Arduino Zero, mais est beaucoup plus petit et aussi moins cher que l'Arduino Zero d'origine (oui, vous pouvez acheter un faux Arduino Zero pour 10 $ sur AliExpress ... mais nous voulons construire sur l'original). Cela nous permettra de créer une petite console portable. Vous pouvez adapter ce projet pour Arduino Zero presque sans effort, seul le résultat sera assez encombrant.

Nous avons également créé une petite carte de test qui implémente une console portable pour les pauvres. Détails ci-dessous!


Nous n'utiliserons pas le framework Arduino. Il n'est pas bien adapté à l'optimisation et la gestion des équipements. (Et ne parlons pas de l'IDE!)

Dans cet article, nous allons décrire comment nous sommes arrivés à la version finale du jeu, décrire toutes les optimisations et critères utilisés. Le jeu lui-même n'est pas encore terminé, il manque de son, de niveaux, etc. Cependant, il peut être utilisé comme point de départ pour de nombreux types de jeux différents!

De plus, il existe de nombreuses autres options d'optimisation, même sans assembleur!

Commençons donc notre voyage!

Des difficultés


En fait, le projet comporte deux aspects complexes: les horaires et la mémoire (à la fois RAM et stockage).

La mémoire


Commençons par la mémoire. Tout d'abord, au lieu de stocker une image de grand niveau, nous utilisons des tuiles. En fait, si vous analysez attentivement la plupart des plateformes, vous remarquerez qu'elles sont créées à partir d'un petit nombre d'éléments graphiques (tuiles) qui sont répétés plusieurs fois.


Turrican 2 sur Amiga. L'un des meilleurs jeux de plateforme de tous les temps. Vous pouvez facilement voir les tuiles dedans!

Le monde / niveau semble diversifié grâce à différentes combinaisons de tuiles. Cela économise beaucoup de mémoire sur le lecteur, mais ne résout pas le problème d'un énorme tampon de trame.

La deuxième astuce que nous utilisons est possible en raison de la puissance de calcul assez importante de uC et de la présence de DMA! Au lieu de stocker toutes les données d'image dans la RAM (et pourquoi est-ce nécessaire?) Nous allons créer une scène dans chaque image à partir de zéro. En particulier, nous continuerons à utiliser des tampons, mais de manière à ce qu'ils tiennent dans un bloc horizontal de graphiques de données d'une hauteur de 16 pixels.

Timings - CPU


Lorsqu'un ingénieur doit créer quelque chose, il vérifie d'abord si cela est possible. Bien sûr, au tout début, nous avons effectué ce test!

Il nous faut donc au moins 25 ips sur un écran de 160 × 128 pixels. Cela représente 512 000 pixels / s. Étant donné que le microcontrôleur fonctionne à une fréquence de 48 MHz, nous avons au moins 93 cycles d'horloge par pixel. Cette valeur tombe à 58 cycles si nous visons 40 fps.

En fait, notre microcontrôleur est capable de traiter jusqu'à 2 pixels à la fois, car chaque pixel prend 16 bits, et l'ATSAMD21 possède un bus interne 32 bits, c'est-à-dire que les performances seront encore meilleures!

Une valeur de 93 cycles d'horloge nous indique que la tâche est complètement réalisable! En fait, nous pouvons conclure que le CPU seul peut gérer toutes les tâches de rendu sans DMA. Cela est très probablement vrai, surtout lorsque vous travaillez avec un assembleur. Cependant, le code sera très difficile à gérer. Et en C ça doit être très optimisé! En fait, Cortex M0 + n'est pas aussi convivial pour C que Cortex M3, et il manque beaucoup d'instructions (il ne charge même pas / enregistre avec l'incrémentation / décrémentation suivante / préliminaire!), Qui doit être implémenté avec deux ou plusieurs instructions simples.

Voyons ce que nous devons faire pour dessiner deux terrains de jeu (en supposant que nous connaissons déjà les coordonnées x et y, etc.).

  • Calculez l'emplacement du pixel de premier plan dans la mémoire flash.
  • Obtenez la valeur en pixels.
  • S'il est transparent, calculez la position du pixel d'arrière-plan dans le flash.
  • Obtenez la valeur en pixels.
  • Calculez l'emplacement cible.
  • Enregistrez le pixel dans la mémoire tampon.

De plus, pour chaque image-objet pouvant entrer dans le tampon, les opérations suivantes doivent être effectuées:

  • Calculez la position d'un pixel sprite dans la mémoire flash.
  • Obtention de la valeur en pixels.
  • S'il n'est pas transparent, calculez l'emplacement du tampon de destination.
  • Enregistrement d'un pixel dans le tampon.

Non seulement toutes ces opérations ne sont pas implémentées comme une seule instruction ASM, mais chaque instruction ASM nécessite deux cycles lors de l'accès à la mémoire RAM / flash.

De plus, nous n'avons toujours pas de logique de jeu (qui, heureusement, prend un peu de temps, car il est calculé une fois par image), la reconnaissance des collisions, le traitement du tampon et les instructions nécessaires pour envoyer des données via SPI.

Par exemple, voici le pseudo-code de ce que nous devons faire (pour l'instant, nous supposons que le jeu n'a pas de défilement et que le terrain de jeu a un fond de couleur constant!) Uniquement pour le premier plan.

Soit cameraY et cameraX les coordonnées du coin supérieur gauche de l'écran dans le monde du jeu.

Soit xTilepos et yTilepos la position de la tuile actuelle sur la carte.

xTilepos = cameraX / 16; // this is a rightward shift of 4 bits. yTilepos = cameraY / 16; destBufferAddress = &buffer[0][0]; for tile = 0...9 nTile = gameMap[yTilepos][xTilepos]; tileDataAddress = &tileData[nTile]; xTilepos = xTilepos + 1; for y = 0…15 for x = 0…15 pixel = *tileDataAddress; tileDataAddress = tileDataAddress + 1; *destBufferAddress = pixel; destBufferAddress = destBufferAddress + 1; next destBufferAddress = destBufferAddress + 144; // point to next row next destBufferAddress = destBufferAddress – ( 160 * 16 - 16); // now point to the position where the next tile will be saved. next 

Le nombre d'instructions pour 2560 pixels (160 x 16) est d'environ 16k, soit 6 par pixel. En fait, vous pouvez dessiner deux pixels à la fois. Cela divise par deux le nombre réel d'instructions par pixel, c'est-à-dire que le nombre d'instructions de haut niveau par pixel est d'environ 3. Cependant, certaines de ces instructions de haut niveau seront soit divisées en deux instructions d'assembleur ou plus, soit nécessiteront au moins deux cycles pour être exécutées car à la mémoire. De plus, nous n'avons pas envisagé de réinitialiser le pipeline du processeur en raison de sauts et des états d'attente pour la mémoire flash. Oui, nous sommes encore loin des cycles 58-93 à notre disposition, mais nous devons encore prendre en compte le contexte du terrain de jeu et les sprites.

Bien que nous constations que le problème peut être résolu sur un seul processeur, le DMA sera beaucoup plus rapide. L'accès direct à la mémoire laisse encore plus de place aux sprites d'écran ou à de meilleurs effets graphiques (par exemple, nous pouvons implémenter le mélange alpha).

Nous verrons que pour configurer le DMA pour chaque tuile, nous avons besoin de moins de 100 instructions C, soit moins de 0,5 par pixel! Bien sûr, DMA devra toujours effectuer le même nombre de transferts en mémoire, mais l'incrémentation et la transmission d'adresse sont effectuées sans l'intervention du CPU, ce qui peut faire autre chose (par exemple, calculer et rendre des sprites).

En utilisant la minuterie SysTick, nous avons découvert que le temps nécessaire pour préparer le DMA pour le bloc entier, puis pour terminer le DMA, est d'environ 12k cycles d'horloge. Remarque: les cycles d'horloge! Pas d'instructions de haut niveau! Le nombre de cycles est assez élevé pour seulement 2560 pixels, soit 1280 mots 32 bits. En fait, nous obtenons environ 10 cycles par mot de 32 bits. Cependant, vous devez tenir compte du temps requis pour préparer le DMA, ainsi que du temps nécessaire au DMA pour charger les descripteurs de transfert à partir de la RAM (qui contiennent essentiellement des pointeurs et le nombre d'octets transférés). De plus, il y a toujours une sorte de changement de bus mémoire (pour que le CPU ne reste pas inactif sans données), et la mémoire flash nécessite au moins un état d'attente.

Horaires - SPI


Un autre goulot d'étranglement est SPI. Est-ce que 12 MHz suffit pour 25 fps? La réponse est oui: 12 MHz correspond à environ 36 images par seconde. Si nous utilisons 24 MHz, la limite doublera!

Soit dit en passant, les spécifications de l'écran et du microcontrôleur indiquent que la vitesse SPI maximale est respectivement de 15 et 12 MHz. Nous avons testé et veillé à ce qu'elle puisse être portée à 24 MHz sans problème, au moins dans la «direction» dont nous avons besoin (le microcontrôleur écrit sur l'écran).

Nous utiliserons le populaire écran SPI de 1,8 pouces. Nous nous sommes assurés que ILI9163 et ST7735 fonctionnent normalement avec une fréquence de 12 MHz (au moins avec 12 MHz. Il est vérifié que le ST7735 fonctionne avec une fréquence allant jusqu'à 24 MHz). Si vous souhaitez utiliser le même affichage que dans le didacticiel «Comment lire des vidéos sur Arduino Uno», nous vous recommandons de le modifier au cas où vous souhaiteriez ajouter la prise en charge SD à l'avenir. Nous utilisons la version de la carte SD afin d'avoir beaucoup d'espace pour d'autres éléments, tels que le son ou des niveaux supplémentaires.

Graphisme


Comme déjà mentionné, le jeu utilise des tuiles. Chaque niveau sera composé de tuiles se répétant selon le tableau, que nous avons appelé "gameMap". Quelle sera la taille de chaque tuile? La taille de chaque tuile affecte considérablement la consommation de mémoire, les détails et la flexibilité (et, comme nous le verrons plus tard, la vitesse aussi). Des tuiles trop grandes nécessiteront la création d'une nouvelle tuile pour chaque petite variation dont nous avons besoin. Cela prendra beaucoup d'espace sur le disque.


Deux carreaux de 32 × 32 pixels (gauche et centre), qui diffèrent dans une petite partie (la partie supérieure droite du pixel est de 16 × 16). Par conséquent, nous devons stocker deux tuiles différentes avec une taille de 32 × 32 pixels. Si nous utilisons une tuile 16 × 16 pixels (à droite), nous devons stocker uniquement deux tuiles 16 × 16 (une tuile entièrement blanche et une tuile à droite). Cependant, lorsque vous utilisez des tuiles 16 × 16, nous obtenons 4 éléments de carte.

Cependant, moins de tuiles par écran sont nécessaires, ce qui augmente la vitesse (voir ci-dessous) et réduit la taille de la carte (c'est-à-dire le nombre de lignes et de colonnes dans le tableau) de chaque niveau. Des tuiles trop petites créent le problème inverse. Les tables de cartes s'agrandissent et la vitesse ralentit. Bien sûr, nous ne prendrons pas de décisions stupides. par exemple, sélectionnez des carreaux d'une taille de 17 × 31 pixels. Notre fidèle ami - deux degrés! Le format 16 × 16 est presque la «règle d'or», il est utilisé dans de nombreux jeux, et nous le choisirons!

Notre écran a une taille de 160 × 128. En d'autres termes, nous avons besoin de 10 × 8 tuiles par écran, c'est-à-dire 80 entrées dans le tableau. Pour un grand niveau d'écrans 10 × 10 (ou 100 × 1 écrans), seuls 8 000 enregistrements seront nécessaires (16 Ko si nous utilisons 16 bits pour l'enregistrement. Plus tard, nous montrerons pourquoi nous avons décidé de choisir 16 bits pour l'enregistrement).

Comparez cela avec la quantité de mémoire susceptible d'être occupée par une grande image sur tout l'écran: 40 Ko * 100 = 4 Mo! C'est fou!

Parlons du système de rendu.

Chaque cadre doit contenir (dans l'ordre des dessins):

  • graphiques d'arrière-plan (terrain de jeu arrière)
  • le graphique de niveau lui-même (premier plan).
  • sprites
  • texte / superposition supérieure.

En particulier, nous effectuerons séquentiellement les opérations suivantes:

  1. Arrière-plan du dessin + premier plan (tuiles)
  2. dessin de carreaux translucides + sprites + superposition supérieure
  3. envoi de données par SPI.

Le fond et les carreaux entièrement opaques seront dessinés par DMA. Une tuile entièrement opaque est une tuile dans laquelle il n'y a pas de pixels transparents.


Carreau partiellement transparent (gauche) et complètement opaque (droite). Dans une tuile partiellement transparente, certains pixels (en bas à gauche) sont transparents, et donc un arrière-plan est visible à travers cette zone.

Les tuiles, les sprites et les superpositions partiellement transparents ne peuvent pas être rendus efficacement par DMA. En fait, le système DMA de la puce ATSAMD21 copie simplement les données et, contrairement au Blitter de l'ordinateur Amiga, il ne vérifie pas la transparence (définie par la valeur de couleur). Tous les éléments partiellement transparents sont dessinés par le CPU.


Les données sont ensuite transmises à l'écran à l'aide de DMA.

Création d'un pipeline


Comme vous pouvez le voir, si nous effectuons ces opérations séquentiellement dans un seul tampon, cela prendra beaucoup de temps. En fait, pendant que le DMA est en cours d'exécution, le CPU ne sera pas occupé, sauf en attendant que le DMA se termine! C'est une mauvaise façon d'implémenter un moteur graphique. De plus, lorsque DMA envoie des données à un appareil SPI, il n'utilise pas sa pleine bande passante. En fait, même lorsque SPI fonctionne à une fréquence de 24 MHz, les données ne sont transmises qu'à une fréquence de 3 MHz, ce qui est assez petit. En d'autres termes, le DMA n'est pas utilisé à son plein potentiel: le DMA peut effectuer d'autres tâches sans vraiment perdre les performances.

C'est pourquoi nous avons implémenté le pipeline, qui est le développement de l'idée de double tampon (nous utilisons trois tampons!). Bien sûr, au final, les opérations sont toujours effectuées de manière séquentielle. Mais le CPU et le DMA effectuent simultanément des tâches différentes, sans (surtout) s'influencer mutuellement.

Voici ce qui se passe simultanément:

  • Le tampon est utilisé pour dessiner des données d'arrière-plan en utilisant le canal DMA 1;
  • Dans un autre tampon (qui était auparavant rempli de données d'arrière-plan), le CPU dessine des sprites et des tuiles partiellement transparentes;
  • Ensuite, un autre tampon (qui contient un bloc de données horizontal complet) est utilisé pour envoyer des données à l'affichage via SPI en utilisant le canal DMA 0. Bien sûr, le tampon utilisé pour envoyer des données via SPI a été précédemment rempli de sprites tandis que le SPI a envoyé le bloc précédent et tandis qu'un autre tampon rempli de tuiles.



DMA


Le système DMA à puce ATSAMD21 n'est pas comparable à Blitter, mais il a néanmoins ses propres fonctionnalités utiles. Grâce au DMA, nous pouvons fournir un taux de rafraîchissement très élevé, malgré un double terrain de jeu.

La configuration du transfert DMA est stockée dans la RAM, dans des «descripteurs DMA», indiquant au DMA comment et où il doit effectuer le transfert en cours. Ces descripteurs peuvent être réunis: s'il y a une connexion (c'est-à-dire qu'il n'y a pas de pointeur nul), puis une fois le transfert terminé, le DMA recevra automatiquement le descripteur suivant. Grâce à l'utilisation de descripteurs multiples, le DMA peut effectuer des «transferts complexes», qui sont utiles lorsque, par exemple, le tampon source est une séquence de segments non contigus d'octets contigus. Cependant, il faut du temps pour obtenir et écrire des descripteurs, car vous devez enregistrer / charger 16 octets de descripteur à partir de la RAM.

Le DMA peut fonctionner avec des données de différentes longueurs: octets, demi-mots (16 bits) et mots (32 bits). Dans la spécification, cette longueur est appelée «taille de battement». Pour SPI, nous sommes obligés d'utiliser le transfert d'octets (bien que la spécification REVD actuelle stipule que les puces SERCOM ATSAMD21 ont FIFO, qui, selon Microchip, peut accepter des données 32 bits, en fait, il semble qu'elles n'aient pas FIFO. La spécification REVD mentionne également Registre SERCOM CTRLC, qui est absent à la fois dans les fichiers d'en-tête et dans la section de description du registre.Heureusement, contrairement à AVR, ATSAMD21 a au moins un registre de données de transmission tamponné, il n'y aura donc pas de pause dans la transmission!). Pour dessiner des tuiles, nous utilisons bien sûr 32 bits. Cela vous permet de copier deux pixels par battement. La puce DMA ATSAMD21 permet également à chaque battement source d'augmenter l'adresse source ou de destination d'un nombre fixe de tailles de battement.

Ces deux aspects sont très importants et déterminent la façon dont nous dessinons les tuiles.

Premièrement, si nous rendions un pixel par battement (16 bits), nous diviserions par deux le débit de notre système. Nous ne pouvons pas refuser la pleine bande passante!

Cependant, si nous dessinons deux pixels par battement, le champ de jeu ne pourra faire défiler qu'un nombre pair de pixels, ce qui provoquera un mouvement fluide. Pour gérer cela, vous pouvez utiliser un tampon plus grand de deux pixels ou plus. Lors de l'envoi de données à l'écran, nous utiliserons le décalage correct (0 ou 1 pixel), selon que nous devons déplacer la «caméra» d'un nombre pair ou impair de pixels.

Cependant, par souci de simplicité, nous réservons de l'espace pour 11 carreaux complets (160 + 16 pixels), et non pour 160 + 2 pixels. Cette approche présente un gros avantage: nous n'avons pas à calculer et à mettre à jour l'adresse du destinataire de chaque descripteur DMA (cela nécessiterait plusieurs instructions, ce qui pourrait entraîner trop de calculs par mosaïque). Bien sûr, nous ne dessinerons que le nombre minimum de pixels, c'est-à-dire pas plus de 162. Oui, au final, nous dépenserons un peu de mémoire supplémentaire (en tenant compte de trois tampons, cela fait environ 1500 octets) pour la vitesse et la simplicité. Vous pouvez également effectuer d'autres optimisations.


Tous les tampons de bloc de 16 lignes (sans descripteurs) sont visibles dans cette animation GIF. À droite, ce qui est réellement affiché. Les 32 premières images sont affichées en GIF, sur lequel nous déplaçons 1 pixel vers la droite dans chaque image. La zone noire du tampon est la partie qui n'est pas mise à jour et son contenu reste simplement des opérations précédentes. Lorsque l'écran fait défiler un nombre impair d'images, une zone de 162 pixels de large est dessinée dans le tampon. Cependant, leur première et dernière colonne (qui sont mises en surbrillance dans l'animation) sont supprimées. Lorsque la valeur de défilement est un multiple de 16 pixels, les opérations de dessin dans le tampon commencent à partir de la première colonne (x = 0).

Et le défilement vertical?

Nous nous en occuperons après avoir montré une méthode de stockage des tuiles dans la mémoire flash.

Comment stocker les carreaux


Une approche naïve (qui nous conviendrait si nous rendions uniquement via le CPU) serait de stocker les tuiles dans la mémoire flash sous la forme d'une séquence de couleurs de pixels. Le premier pixel de la première ligne, le second, et ainsi de suite, jusqu'au seizième. Ensuite, nous enregistrons le premier pixel de la deuxième ligne, le second, etc.

Pourquoi une telle décision est-elle naïve? Parce que dans ce cas, DMA ne peut rendre que 16 pixels par descripteur DMA! Par conséquent, nous aurons besoin de 16 descripteurs, dont chacun a besoin de 4 + 4 opérations d'accès à la mémoire (c'est-à-dire, pour transférer 32 octets - 8 opérations de lecture en mémoire + 8 opérations d'écriture en mémoire - DMA doit effectuer 4 lectures supplémentaires + 4 écritures). C'est assez inefficace!

En fait, pour chaque descripteur, DMA ne peut incrémenter les adresses source et destination que d'un nombre fixe de mots. Après avoir copié la première ligne de la tuile dans le tampon, l'adresse du destinataire ne doit pas être augmentée d'un mot, mais d'une valeur telle qu'elle pointe vers la ligne suivante du tampon. Cela n'est pas possible car chaque descripteur de transmission indique uniquement l'incrément de transmission de battement, qui ne peut pas être modifié.

Il sera beaucoup plus intelligent d'envoyer les deux premiers pixels de chaque ligne de la tuile séquentiellement, c'est-à-dire les pixels 0 et 1 de la ligne 0, les pixels 0 et 1 de la ligne 1, etc., aux pixels 0 et 1 de la ligne 15. Ensuite, nous envoyons les pixels 2 et 3 de la ligne 0, etc.


Comment une tuile est-elle stockée?

Dans la figure ci-dessus, chaque nombre indique l'ordre dans lequel le pixel 16 bits est stocké dans le réseau de tuiles.

Cela peut être fait avec un descripteur, mais nous avons besoin de deux choses:

  • Les mosaïques doivent être stockées de sorte que lors de l'incrémentation de la source d'un mot, nous indiquions toujours les positions de pixels correctes. En d'autres termes, si (r, c) est un pixel dans la ligne r et la colonne c, alors nous devons enregistrer les pixels (0,0) (0,1) (1,0) (1,1) (2,0) séquentiellement (2,1) ... (15,0) (15,1) (0,2) (0,3) (1,2) (1,3) ...
  • Le tampon doit avoir une largeur de 256 pixels (pas 160)

Le premier objectif est très facile à atteindre: il suffit de changer l'ordre des données, vous pouvez le faire lors de l'exportation de graphiques vers un fichier c (voir image ci-dessus).

Le deuxième problème peut être résolu car DMA vous permet d'augmenter l'adresse du destinataire après chaque battement de 512 octets. Cela a deux conséquences:

  • Nous ne pouvons pas envoyer de données à l'aide d'un seul descripteur sur un bloc SPI. Ce n'est pas un problème très grave, car au final nous lisons un descripteur sur 160 pixels. L'impact sur les performances sera minime.
  • Le bloc doit avoir une taille de 256 * 2 * 16 octets = 8 Ko, et il y aura beaucoup "d'espace inutilisé" dedans.

Cependant, cet espace peut toujours être utilisé, par exemple, pour des descripteurs.

En fait, chaque descripteur a une taille de 16 octets. Nous avons besoin d'au moins 10 * 8 (et en fait 11 * 8!) Descripteurs pour les tuiles et 16 descripteurs pour SPI.

C'est pourquoi plus il y a de tuiles, plus la vitesse est élevée. En fait, si nous utilisions, par exemple, une tuile 32 x 32, nous aurions alors besoin de moins de descripteurs par écran (320 au lieu de 640). Cela réduirait le gaspillage de ressources.

Afficher le bloc de données


Le tampon de bloc, les descripteurs et les autres données sont stockés dans un type de structure, que nous avons appelé displayBlock_t.

displayBlock est un tableau de 16 éléments displayLineData_t. Les données DisplayLine contiennent 176 pixels plus 80 mots. Dans ces 80 mots, nous stockons des descripteurs d'affichage ou d'autres données d'affichage utiles (en utilisant l'union).



Comme nous avons 16 lignes, chaque mosaïque à la position X utilise les 8 premiers descripteurs DMA (0 à 7) des lignes X. Comme nous avons un maximum de 11 mosaïques (la ligne d'affichage fait 176 pixels de large), les mosaïques n'utilisent que les premiers descripteurs DMA 11 lignes de données. Les descripteurs 8 à 9 de toutes les lignes et les descripteurs 0 à 9 des lignes 11 à 15 sont libres.

Parmi ceux-ci, les descripteurs 8 et 9 des lignes 0 à 7 seront utilisés pour SPI.

Les descripteurs 0..9 lignes 11-15 (jusqu'à 50 descripteurs, bien que nous n'en utiliserons que 48) seront utilisés pour le terrain de jeu en arrière-plan.

La figure ci-dessous montre leur structure.


Terrain de jeu d'arrière-plan


Le terrain de jeu en arrière-plan est géré différemment. Premièrement, si nous avons besoin d'un défilement fluide, nous devrons revenir au format à deux pixels, car le premier plan et l'arrière-plan défileront à des vitesses différentes. Par conséquent, le rythme sera à mi-parcours. Bien qu'il s'agisse d'un inconvénient en termes de vitesse, cette approche facilite l'intégration. Il ne nous reste qu'un petit nombre de descripteurs, donc les petites tuiles ne peuvent pas être utilisées. De plus, pour simplifier le travail et ajouter rapidement la parallaxe, nous utiliserons de longs «secteurs».

L'arrière-plan n'est dessiné que s'il y a au moins un pixel partiellement transparent. Cela signifie que s'il n'y a qu'une seule tuile transparente, l'arrière-plan sera dessiné. Bien sûr, c'est un gaspillage de bande passante, mais cela simplifie tout.

Comparez l'arrière-plan et les terrains de jeu avant:

  • En arrière-plan, des secteurs sont utilisés, qui sont de longues tuiles stockées de manière "naïve".
  • L'arrière-plan a sa propre carte, mais horizontalement il se répète. Grâce à cela, moins de mémoire est utilisée.
  • Le fond a une parallaxe pour chaque secteur.

Terrain de jeu avant


Comme il a été dit, dans chaque bloc, nous avons jusqu'à 11 tuiles (10 tuiles complètes, ou 9 tuiles complètes et 2 fichiers partiels). Chacune de ces tuiles, si elle n'est pas marquée comme transparente, DMA est dessinée. S'il n'est pas complètement opaque, il est ajouté à la liste, qui sera analysée plus tard, lors du rendu des sprites.

Nous connectons ensemble deux terrains de jeu


Les descripteurs du terrain de jeu d'arrière-plan (qui sont toujours calculés) et du terrain de jeu avant forment une très longue liste chaînée. La première partie dessine un terrain de jeu d'arrière-plan. La deuxième partie dessine des tuiles sur le fond. La longueur de la deuxième partie peut être variable, car les descripteurs DMA des tuiles partiellement transparentes sont exclus de la liste. Si le bloc ne contient que des tuiles opaques, le DMA est configuré comme suit. pour démarrer directement à partir du premier descripteur de la première tuile.

Sprites et tuiles avec transparence


Les carreaux avec transparence et sprites sont traités de la même manière. L'analyse des pixels de tuile / sprite est effectuée.S'il est noir, il est transparent, et par conséquent, la vignette d'arrière-plan ne change pas. S'il n'est pas noir, le pixel d'arrière-plan est remplacé par un pixel sprite / tuile.

Défilement vertical


Lorsque vous travaillez avec le défilement horizontal, nous dessinons jusqu'à 11 tuiles, même si lors du dessin de 11 tuiles, la première et la dernière ne sont que partiellement dessinées. Un tel rendu partiel est possible du fait que chaque descripteur dessine deux colonnes de la tuile, afin que nous puissions facilement définir le début et la fin de la liste liée.

Lorsque vous travaillez avec le défilement vertical, nous devons calculer à la fois le registre du récepteur et le volume de transmission. Ils doivent être définis plusieurs fois par image. Pour éviter cette agitation, nous pouvons simplement dessiner jusqu'à 9 blocs complets par image (8 si le défilement est un multiple de 16).

Équipement


Comme nous l'avons dit, le cœur du système est uChip. Et le reste?

Voici un schéma! Certains aspects méritent d'être mentionnés.


Clés


Pour optimiser l'utilisation des E / S, nous utilisons une petite astuce. Nous aurons 4 bus de capteurs L1-L4 et un fil LC commun. 1 et 0 sont appliqués alternativement sur le fil commun. En conséquence, les bus de capteurs seront alternativement abaissés ou relevés à l'aide de résistances de rappel internes. Deux clés sont connectées entre chacun des bus de clés et un bus commun. Une diode est insérée en série avec ces deux touches. Chacune de ces diodes est commutée dans le sens opposé, de sorte qu'à chaque fois une seule touche est "lue".

Puisqu'il n'y a pas de contrôleur de clavier intégré (et qu'aucun contrôleur de clavier intégré n'utilise cette méthode intéressante), huit touches sont rapidement interrogées au début de chaque image. Puisque les entrées doivent être tirées vers le haut et vers le bas, nous ne pouvons pas (et ne voulons pas) utiliser des résistances externes, nous devons donc utiliser des résistances intégrées, qui peuvent avoir une résistance assez élevée (60 kOhm). Cela signifie que lorsque le bus commun change d'état et que les bus de données changent leur état de traction haut / bas, vous devez insérer un certain délai pour que la résistance de traction haut / bas intégrée modifie le contrat et règle la capacité parasite au niveau souhaité. Mais nous ne voulons pas attendre! Par conséquent, nous mettons le bus commun dans un état de haute impédance (afin qu'il n'y ait pas de désaccord), et passons d'abord les bus de capteur aux valeurs logiques 1 ou 0,en les configurant temporairement en sortie. Plus tard, ils sont configurés en entrée en tirant vers le haut ou vers le bas. La résistance de sortie étant de l'ordre de dizaines d'Ohms, l'état change en quelques nanosecondes, c'est-à-dire que lorsque le bus du capteur repasse en entrée, il sera déjà dans l'état souhaité. Après cela, le bus commun passe à la sortie avec la polarité opposée.

Cela améliore considérablement la vitesse de numérisation et élimine le besoin de retard / instructions nop.

Connexion SPI


Nous avons connecté le SD et l'écran afin qu'ils communiquent entre eux sans transférer de données vers l'ATSAMD21. Cela peut être utile si vous souhaitez lire la vidéo.

Les résistances reliant MISO et MOSI doivent être faibles. S'ils sont trop grands, le SPI ne fonctionnera pas, car le signal sera trop faible.

Optimisation et développement ultérieur


L'un des plus gros problèmes est l'utilisation de la RAM. Trois blocs occupent chacun 8 Ko, ne laissant que 8 Ko par pile et autres variables. Pour le moment, nous n'avons que 1,3 Ko de RAM libre + 4 Ko de pile (4 Ko par pile - c'est beaucoup, peut-être allons-nous le réduire).

Cependant, vous pouvez utiliser des blocs d'une hauteur non pas de 16, mais de 8 pixels. Cela augmentera le gaspillage de ressources sur les descripteurs DMA, mais réduira presque de moitié la quantité de mémoire occupée par le tampon de bloc (notez que le nombre de descripteurs ne changera pas si nous continuons à utiliser des tuiles 16 × 16, nous devrons donc changer la structure du bloc). Cela peut libérer environ 7,5 Ko de RAM, ce qui sera très utile pour implémenter des fonctions telles qu'une carte modifiable avec des secrets ou ajouter du son (bien que le son puisse être ajouté même avec 1 Ko de RAM).

Un autre problème est le sprite, mais cette modification est beaucoup plus simple à effectuer et vous n'avez besoin que de la fonction createNextFrameScene (). En fait, nous créons en RAM un énorme tableau avec l'état de tous les sprites. Ensuite, pour chaque image-objet, nous calculons si sa position se situe dans la zone de l'écran, puis l'animons et l'ajoutons à la liste de rendu.

Au lieu de cela, vous pouvez effectuer une optimisation. Par exemple, dans gameMap, vous pouvez stocker non seulement la valeur de la tuile, mais également un indicateur indiquant la transparence de la tuile, défini dans l'éditeur. Cela nous permettra de vérifier rapidement si la tuile doit être rendue: DMA ou CPU. C'est pourquoi nous avons utilisé des enregistrements 16 bits pour la carte de tuiles. Si nous supposons que nous avons un ensemble de 256 tuiles (pour le moment, nous avons moins de 128 tuiles, mais qu'il y a suffisamment d'espace sur la mémoire flash pour en ajouter de nouvelles), alors il y a 7 bits libres qui peuvent être utilisés à d'autres fins. Trois de ces sept bits peuvent être utilisés pour indiquer si une image-objet / objet est stockée. Par exemple:

0b000 =
0b001 =
0b010 =
0b011 =
0b100 =
0b101 =
0b110 =
0b111 = , , .


Ensuite, vous pouvez créer une table de bits dans la RAM dans laquelle chaque bit signifie si (par exemple, un ennemi) est détecté / si (par exemple, un bonus) est ramassé / si un certain objet est activé (commutateur). Au niveau des écrans 10 × 10, cela nécessitera 8000 bits, soit 1 Ko de RAM. Le bit est réinitialisé lorsqu'un ennemi est détecté ou qu'un bonus est récupéré.

Dans createNextFrameScene (), nous devons vérifier les bits correspondant aux tuiles dans la zone visible actuelle. S'ils ont une valeur de 1:

  • S'il s'agit d'un bonus, ajoutez-le simplement à la liste des sprites pour le rendu.
  • S'il s'agit d'un ennemi, créez un sprite dynamique et réinitialisez le drapeau. Dans l'image suivante, la scène contiendra un sprite dynamique jusqu'à ce que l'ennemi quitte l'écran ou soit tué.

Cette approche présente des inconvénients.

  1. -, ( ). .
  2. -, 80 , , . , 32 . , «/» ( «», .. 0!). «», «» ( ).
  3. -, . ( ), . , .
  4. -, , , , . , , . , , , , !
  5. , (, Unreal Tournament , ).

Néanmoins, de cette manière, nous pouvons stocker et traiter les sprites à un niveau beaucoup plus efficace.

Cependant, cette technique est plus pertinente pour la "logique du jeu" que pour le moteur graphique du jeu.

Peut-être qu'à l'avenir, nous mettrons en œuvre cette fonction.

Pour résumer


Nous espérons que vous avez apprécié cet article d'introduction. Nous devons expliquer de nombreux autres aspects qui feront l'objet de futurs articles.

En attendant, vous pouvez télécharger le code source complet du jeu! Si vous l'aimez, vous pouvez soutenir financièrement l'artiste ansimuz , qui a dessiné tous les graphiques et les a donnés au monde gratuitement. Nous acceptons également les dons .

Le jeu n'est pas encore terminé. Nous voulons ajouter du son, de nombreux niveaux, des objets avec lesquels vous pouvez interagir et autres. Vous pouvez créer vos propres modifications! Nous espérons voir de nouveaux jeux avec de nouveaux graphismes et niveaux!

Bientôt, nous publierons un éditeur de carte, mais pour l'instant il est trop rudimentaire pour le montrer à la communauté!

Vidéo


(Remarque: en raison d'un mauvais éclairage, la vidéo a été enregistrée à une fréquence d'images beaucoup plus faible! Bientôt, nous mettrons à jour la vidéo afin que vous puissiez estimer la vitesse maximale à 40 ips!)


Gratitude


Les graphismes du jeu (et les tuiles montrées sur certaines images) sont tirés de l' actif gratuit «Sunny Land» créé par ansimuz .

Documents téléchargeables


Le code source du projet est dans le domaine public, c'est-à-dire qu'il est fourni gratuitement. Nous le partageons dans l'espoir qu'il sera utile à quelqu'un. Nous ne garantissons pas qu'en raison d'un bug / erreur dans le code, il n'y aura pas de problèmes!

Diagramme schématique Projet

KiCad

Projet Atmel Studio 7 (source)

Source: https://habr.com/ru/post/fr466323/


All Articles