Le jeu Snake pour FPGA Cyclone IV (avec joystick VGA et SPI)

Présentation


Vous souvenez-vous du jeu du serpent depuis l'enfance, où un serpent court sur l'écran en train de manger une pomme? Cet article décrit notre implémentation du jeu sur un FPGA 1 .


Gameplay.gif
Figure 1. Gameplay


Tout d'abord, laissez-nous nous présenter et expliquer la raison pour laquelle nous avons travaillé sur le projet. Nous sommes 3: Tymur Lysenko , Daniil Manakovskiy et Sergey Makarov . En tant qu'étudiants de première année de l' Université Innopolis , nous avons suivi un cours sur "l'architecture informatique", qui est enseigné de manière professionnelle et permet à l'apprenant de comprendre la structure de bas niveau d'un ordinateur. À un moment donné au cours, les instructeurs nous ont donné l'occasion de développer un projet de FPGA pour des points supplémentaires dans le cours. Notre motivation n'a pas seulement été la note, mais notre intérêt à acquérir plus d'expérience dans la conception matérielle, à partager les résultats et, enfin, à avoir un jeu agréable.


Maintenant, entrons dans les détails sombres et profonds.


Aperçu du projet


Pour notre projet, nous avons sélectionné un jeu amusant et facile à mettre en œuvre, à savoir le "Serpent". La structure de l'implémentation se présente comme suit: tout d'abord, une entrée est prise à partir d'un joystick SPI, puis traitée, et enfin, une image est sortie sur un moniteur VGA et un score est affiché sur un affichage à 7 segments (en hexadécimal). Bien que la logique du jeu soit intuitive et simple, VGA et le joystick ont ​​été des défis intéressants et leur mise en œuvre a conduit à une bonne expérience de jeu.


Le jeu a les règles suivantes. Un joueur commence avec une seule tête de serpent. L'objectif est de manger des pommes, qui sont générées au hasard sur l'écran après que la précédente a été mangée. En outre, le serpent est étendu d'une queue après avoir satisfait la faim. Les queues se déplacent les unes après les autres, suivant la tête. Le serpent bouge toujours. Si les bordures de l'écran sont atteintes, le serpent est transféré d'un autre côté de l'écran. Si la tête frappe la queue, la partie est terminée.


Outils utilisés


  • Altera Cyclone IV (EP4CE6E22C8N) avec 6272 éléments logiques, horloge intégrée de 50 MHz, VGA couleur 3 bits, affichage à 8 chiffres à 7 segments. Le FPGA ne peut pas prendre d'entrée analogique sur ses broches.
  • Joystick SPI (KY-023)
  • Un moniteur VGA qui prend en charge un taux de rafraîchissement de 60 Hz
  • Quartus Prime Lite Edition 18.0.0 Build 614
  • Verilog HDL IEEE 1364-2001
  • Planche à pain
  • Éléments électriques:
    • 8 connecteurs mâle-femelle
    • 1 connecteur femelle-femelle
    • 1 connecteur mâle-mâle
    • 4 résistances (4,7 KΩ)

Présentation de l'architecture


L'architecture du projet est un facteur important à considérer. La figure 2 montre cette architecture du point de vue supérieur:


Design.png
Figure 2. Vue de dessus de la conception ( pdf )


Comme vous pouvez le voir, il existe de nombreuses entrées, sorties et certains modules. Cette section décrit la signification de chaque élément et précise les broches utilisées sur la carte pour les ports.


Entrées principales


Les principales entrées nécessaires à l'implémentation sont res_x_one , res_x_two , res_y_one , res_y_two , qui sont utilisées pour recevoir la direction actuelle d'un joystick. La figure 3 montre la correspondance entre leurs valeurs et les directions.


EntréeGaucheÀ droiteEn hautVers le basPas de changement de direction
res_x_one (PIN_30)10xx1
res_x_two (PIN_52)10xx0
res_y_one (PIN_39)xx101
res_y_two (PIN_44)xx100

Figure 3. Cartographie des entrées et directions du joystick


Autres entrées


  • clk - l'horloge du tableau (PIN_23)
  • reset - signal pour réinitialiser le jeu et arrêter l'impression (PIN_58)
  • couleur - lorsque 1, toutes les couleurs possibles sont affichées à l'écran et utilisées uniquement à des fins de démonstration (PIN_68)

Modules principaux


joystick_input


joystick_input est utilisé pour produire un code de direction basé sur une entrée du joystick.


game_logic


game_logic contient toute la logique nécessaire pour jouer à un jeu. Le module déplace un serpent dans une direction donnée. De plus, il est responsable de la consommation de pommes et de la détection des collisions. De plus, il reçoit les coordonnées x et y actuelles d'un pixel sur l'écran et renvoie une entité placée à la position.


VGA_Draw


Le tiroir définit la couleur d'un pixel à une valeur particulière en fonction de la position actuelle ( iVGA_X, iVGA_Y ) et de l'entité actuelle ( ent ).


VGA_Ctrl


Génère un train de bits de contrôle vers la sortie VGA ( V_Sync, H_Sync, R, G, B ).


SSEG_Display 2


SSEG_Display est un pilote pour afficher le score actuel sur l'affichage à 7 segments.


Vga_clk


VGA_clk reçoit une horloge de 50 MHz et la réduit à 25,175 MHz.


game_upd_clk


game_upd_clk est un module qui génère une horloge spéciale qui déclenche une mise à jour d'un état de jeu.


Sorties


  • VGA_B - Broche bleue VGA (PIN_144)
  • VGA_G - Broche verte VGA (PIN_1)
  • VGA_R - Broche rouge VGA (PIN_2)
  • VGA_HS - Synchronisation horizontale VGA (PIN_142)
  • VGA_VS - Synchronisation verticale VGA (PIN_143)
  • sseg_a_to_dp - spécifie lequel des 8 segments à allumer (PIN_115, PIN_119, PIN_120, PIN_121, PIN_124, PIN_125, PIN_126, PIN_127)
  • sseg_an - spécifie lequel des 4 écrans à 7 segments doit être utilisé (PIN_128, PIN_129, PIN_132, PIN_133)

Implémentation


Entrée avec joystick SPI


stick.jpg


Figure 4. Joystick SPI (KY-023)


Lors de la mise en œuvre d'un module d'entrée, nous avons découvert que le stick produit un signal analogique. Le joystick a 3 positions pour chaque axe:


  • haut - sortie ~ 5V
  • sortie moyenne - ~ 2,5 V
  • faible - sortie ~ 0V

L'entrée est très similaire au système ternaire: pour l'axe X, nous avons true (gauche), false (droite) et un état undetermined , où le joystick n'est ni à gauche ni à droite. Le problème est que la carte FPGA ne peut traiter qu'une entrée numérique. Par conséquent, nous ne pouvons pas convertir cette logique ternaire en binaire simplement en écrivant du code. La première solution suggérée a été de trouver un convertisseur analogique-numérique, mais nous avons ensuite décidé d'utiliser nos connaissances en physique de l'école et de mettre en œuvre le diviseur de tension 3 . Pour définir les trois états, nous aurons besoin de deux bits: 00 est false , 01 est undefined et 11 est true . Après quelques mesures, nous avons découvert que sur notre carte, la frontière entre zéro et un est d'environ 1,7V. Ainsi, nous avons construit le schéma suivant (image créée à l'aide de circuitlab 4 ):


Stick_connection.png


Figure 5. Circuit pour ADC pour joystick


L'implémentation physique est construite à l'aide des éléments du kit Arduino et se présente comme suit:


stick_imp


Figure 6. Implémentation d'ADC


Notre circuit prend une entrée pour chaque axe et produit deux sorties: la première provient directement du manche et ne devient nulle que si le joystick sort zero . Le second est 0 à un état undetermined , mais toujours 1 à true . C'est le résultat exact que nous attendions.


La logique du module d'entrée est:


  1. Nous traduisons notre logique ternaire en fils binaires simples pour chaque direction;
  2. À chaque cycle d'horloge, nous vérifions si une seule direction est true (le serpent ne peut pas passer par la diagonale);
  3. Nous comparons notre nouvelle direction avec la précédente pour empêcher le serpent de se manger en ne permettant pas au joueur de changer la direction dans la direction opposée.

Une partie du code du module d'entrée
 reg left, right, up, down; initial begin direction = `TOP_DIR; end always @(posedge clk) begin //1 left = two_resistors_x; right = ~one_resistor_x; up = two_resistors_y; down = ~one_resistor_y; if (left + right + up + down == 3'b001) //2 begin if (left && (direction != `RIGHT_DIR)) //3 begin direction = `LEFT_DIR; end //same code for other directions end end 

Sortie vers VGA


Nous avons décidé de faire une sortie avec une résolution 640x480 sur un écran de 60 Hz fonctionnant à 60 FPS.


Le module VGA se compose de 2 parties principales: un pilote et un tiroir . Le pilote génère un train de bits composé de signaux de synchronisation verticale et horizontale et d'une couleur qui est donnée aux sorties VGA. Un article 5 écrit par @SlavikMIPT décrit les principes de base du travail avec VGA. Nous avons adapté le driver de l'article à notre board.


Nous avons décidé de décomposer l'écran en une grille de 40x30 éléments, composée de carrés de 16x16 pixels. Chaque élément représente 1 entité de jeu: soit une pomme, une tête de serpent, une queue ou rien.


L'étape suivante de notre implémentation a été de créer des sprites pour les entités.


Le cyclone IV n'a que 3 bits pour représenter une couleur sur VGA (1 pour le rouge, 1 pour le vert et 1 pour le bleu). En raison d'une telle limitation, nous avons dû implémenter un convertisseur pour adapter les couleurs des images à celles disponibles. À cette fin, nous avons créé un script python qui divise une valeur RVB de chaque pixel par 128.


Le script python
 from PIL import Image, ImageDraw filename = "snake_head" index = 1 im = Image.open(filename + ".png") n = Image.new('RGB', (16, 16)) d = ImageDraw.Draw(n) pix = im.load() size = im.size data = [] code = "sp[" + str(index) + "][{i}][{j}] = 3'b{RGB};\\\n" with open("code_" + filename + ".txt", 'w') as f: for i in range(size[0]): tmp = [] for j in range(size[1]): clr = im.getpixel((i, j)) vg = "{0}{1}{2}".format(int(clr[0] / 128), # an array representation for pixel int(clr[1] / 128), # since clr[*] in range [0, 255], int(clr[2] / 128)) # clr[*]/128 is either 0 or 1 tmp.append(vg) f.write(code.format(i=i, j=j, RGB=vg)) # Verilog code to initialization d.point((i, j), tuple([int(vg[0]) * 255, int(vg[1]) * 255, int(vg[2]) * 255])) # Visualize final image data.append(tmp) n.save(filename + "_3bit.png") for el in data: print(" ".join(el)) 

OriginalAprès le script



Figure 7. Comparaison entre entrée et sortie


Le but principal du tiroir est d'envoyer une couleur d'un pixel à VGA en fonction de la position actuelle ( iVGA_X, iVGA_Y ) et de l'entité actuelle ( ent ). Tous les sprites sont codés en dur mais peuvent être facilement modifiés en générant un nouveau code en utilisant le script ci-dessus.


Logique du tiroir
 always @(posedge iVGA_CLK or posedge reset) begin if(reset) begin oRed <= 0; oGreen <= 0; oBlue <= 0; end else begin // DRAW CURRENT STATE if (ent == `ENT_NOTHING) begin oRed <= 1; oGreen <= 1; oBlue <= 1; end else begin // Drawing a particular pixel from sprite oRed <= sp[ent][iVGA_X % `H_SQUARE][iVGA_Y % `V_SQUARE][0]; oGreen <= sp[ent][iVGA_X % `H_SQUARE][iVGA_Y % `V_SQUARE][1]; oBlue <= sp[ent][iVGA_X % `H_SQUARE][iVGA_Y % `V_SQUARE][2]; end end end 

Sortie sur l'affichage à 7 segments


Afin de permettre au joueur de voir son score, nous avons décidé d'afficher un score de jeu sur l'affichage à 7 segments. En raison du manque de temps, nous avons utilisé le code de la documentation EP4CE6 Starter Board Documentation 2 . Ce module affiche un nombre hexadécimal à l'écran.


Logique du jeu


Au cours du développement, nous avons essayé plusieurs approches, cependant, nous nous sommes retrouvés avec celle qui nécessite une quantité minimale de mémoire, est facile à implémenter dans le matériel et peut bénéficier de calculs parallèles.


Le module remplit plusieurs fonctions. Comme VGA dessine un pixel à chaque cycle d'horloge en commençant par le coin supérieur gauche se déplaçant vers le coin inférieur droit, le module VGA_Draw, qui est responsable de la production d'une couleur pour un pixel, doit identifier la couleur à utiliser pour les coordonnées actuelles. C'est ce que le module de logique de jeu devrait produire - un code d'entité pour les coordonnées données.
De plus, il ne doit mettre à jour l'état du jeu qu'après le plein écran. Un signal produit par le module game_upd_clk est utilisé pour déterminer quand mettre à jour.


État du jeu


L'état du jeu se compose de:


  • Coordonnées de la tête de serpent
  • Un tableau de coordonnées de la queue du serpent. Le tableau est limité par 128 éléments dans notre implémentation
  • Nombre de queues
  • Coordonnées d'une pomme
  • Game over flag
  • Match gagné drapeau

La mise à jour de l'état du jeu comprend plusieurs étapes:


  1. Déplacez la tête du serpent vers de nouvelles coordonnées, en fonction d'une direction donnée. S'il s'avère qu'une coordonnée est sur son bord et qu'elle doit être modifiée davantage, alors la tête doit sauter sur un autre bord de l'écran. Par exemple, une direction est définie à gauche et la coordonnée X actuelle est 0. Par conséquent, la nouvelle coordonnée X doit devenir égale à la dernière adresse horizontale.
  2. De nouvelles coordonnées de la tête du serpent sont testées par rapport aux coordonnées de la pomme:
    2.1. Dans le cas où ils sont égaux et que le tableau n'est pas plein, ajoutez une nouvelle queue au tableau et incrémentez le compteur de queue. Lorsque le compteur atteint sa valeur la plus élevée (128 dans notre cas), le drapeau de jeu gagné est mis en place et cela signifie que le serpent ne peut plus grandir et que le jeu continue. La nouvelle queue est placée sur les coordonnées précédentes de la tête du serpent. Des coordonnées aléatoires pour X et Y doivent être prises pour y placer une pomme.
    2.2. Dans le cas où elles ne sont pas égales, permutez successivement les coordonnées des queues adjacentes. (n + 1) -th queue devrait recevoir les coordonnées de n-th, au cas où la n-ème queue a été ajoutée avant (n + 1) -th. La première queue reçoit les anciennes coordonnées de la tête.
  3. Vérifiez si les nouvelles coordonnées de la tête du serpent coïncident avec les coordonnées d'une queue. Si tel est le cas, le drapeau game over est levé et le jeu s'arrête.

Génération aléatoire de coordonnées


Nombres aléatoires produits en prenant des bits aléatoires générés par des registres à décalage à décalage à rétroaction linéaire (LFSR) 6 bits 6 . Pour adapter les nombres dans un écran, ils sont divisés par les dimensions de la grille de jeu et le reste est pris.


Conclusion


Après 8 semaines de travail, le projet a été mis en œuvre avec succès. Nous avons une certaine expérience dans le développement de jeux et nous sommes retrouvés avec une version agréable d'un jeu "Snake" pour un FPGA. Le jeu est jouable, et nos compétences en programmation, conception d'une architecture et soft-skills se sont améliorées.


Segments reconnus


Nous tenons à exprimer nos remerciements et notre gratitude à nos professeurs Muhammad Fahim et Alexander Tormasov pour nous avoir donné la connaissance profonde et l'opportunité de la mettre en pratique. Nous remercions chaleureusement Vladislav Ostankovich de nous avoir fourni le matériel essentiel utilisé dans le projet et Temur Kholmatov pour son aide au débogage. Nous n'oublierons pas de nous rappeler qu'Anastassiya Boiko a dessiné de magnifiques sprites pour le jeu. Aussi, nous voudrions adresser nos sincères remerciements à Rabab Marouf pour la relecture et l'édition de cet article.


Merci à tous ceux qui nous ont aidés à tester le jeu et à essayer de battre un record. J'espère que vous apprécierez d'y jouer!


Les références


[1]: Projet sur le Github
[2]: [FPGA] Documentation de la carte de démarrage EP4CE6
[3]: Diviseur de tension
[4]: Outil de modélisation des circuits
[5]: Adaptateur VGA pour FPGA Altera Cyclone III
[6]: Registre à décalage à rétroaction linéaire (LFSR) sur Wikipedia
LFSR dans un FPGA - VHDL et code Verilog
Une texture de pomme
Idée pour générer des nombres aléatoires
Palnitkar, S. (2003). Verilog HDL: A Guide to Digital Design and Synthesis, Second Edition.

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


All Articles