Graphiques 3D sur le STM32F103

image

Une courte histoire sur la façon de pousser le non modifiable et d'afficher des graphiques tridimensionnels en temps réel à l'aide d'un contrôleur qui n'a ni vitesse ni mémoire pour cela.

En 2017 (à en juger par la date de modification du fichier), j'ai décidé de passer des contrôleurs AVR à des STM32 plus puissants. Naturellement, le premier contrôleur était le F103, largement diffusé. Il n'est pas moins naturel que l'utilisation de cartes de débogage standard ait été rejetée au profit de la fabrication à partir de zéro selon ses besoins. Curieusement, il n'y avait presque pas de jambages (sauf que l'UART1 devrait être amené à un connecteur normal et non béquillé par le câblage).

Par rapport à l'AVR, les caractéristiques de la pierre sont assez décentes: horloge à 72 MHz (en pratique, vous pouvez overclocker à 100 MHz, voire plus, mais uniquement à vos risques et périls!), 20 Ko de RAM et 64 Ko de flash. De plus, une tonne de périphériques, lors de l'utilisation dont le principal problème est de ne pas avoir peur de cette abondance et de réaliser que vous n'avez pas besoin de pelleter les dix registres pour commencer, il suffit de définir trois bits dans les bons. Au moins jusqu'à ce que vous vouliez quelque chose d'étrange.

Lorsque la première euphorie de la possession d'un tel pouvoir est passée, un désir a surgi de sonder ses limites. Comme exemple efficace, j'ai choisi le calcul de graphiques en trois dimensions avec toutes ces matrices, l'éclairage, les modèles polygonaux et un Z-buffer avec un affichage 320x240 sur le contrôleur ili9341. Les deux problèmes les plus évidents à résoudre sont la vitesse et le volume. Une taille d'écran de 320x240 à 16 bits par couleur donne 150 Ko par image. Mais la RAM totale que nous avons n'est que de 20 Ko ... Et ces 150 Ko doivent être transférés à l'écran au moins 10 fois par seconde, c'est-à-dire que le taux de change devrait être d'au moins 1,5 Mo / s ou 12 Mo / s, ce qui ressemble déjà à une charge importante sur le cœur. Heureusement, dans ce contrôleur, il existe un module RAP (accès direct à la mémoire, alias Direct Memory Access, DMA), qui vous permet de ne pas charger le noyau avec des opérations de transfusion de vide en vide.Autrement dit, vous pouvez préparer le tampon, dire au module «ici vous avez le tampon de données, travaillez!», Et à ce moment préparer les données pour le prochain transfert. Et en tenant compte de la capacité de l'écran à recevoir des données dans un flux, l'algorithme suivant émerge: le tampon avant est mis en évidence, à partir duquel le DMA transfère les données à l'écran, le tampon arrière dans lequel le rendu a lieu et le tampon Z utilisé pour la découpe en profondeur. Les tampons sont une seule ligne (ou colonne, peu importe) de l'affichage. Et au lieu de 150 Ko, nous n'avons besoin que de 1920 octets (320 pixels par ligne * 3 tampons * 2 octets par point), ce qui tient parfaitement en mémoire. Le deuxième hack est basé sur le fait que le calcul des matrices de transformation et des coordonnées des sommets ne peut pas être effectué pour chaque ligne, sinon l'image sera déformée de la manière la plus bizarre et sa vitesse est désavantageuse. Au lieu de cela, des calculs "externes",c'est-à-dire que la multiplication des matrices de transformation et leur application aux sommets sont recalculées sur chaque image, puis converties en une représentation intermédiaire, qui est optimisée pour le rendu en une image 320x1.

Pour des raisons de hooligan, la bibliothèque ressemblera à OpenGL de l'extérieur. Comme dans l'OpenGL d'origine, le rendu commence par la formation de la matrice de transformation - la suppression de glLoadIdentity () crée l'unité de matrice actuelle, puis un ensemble de transformations glRotateXY (...), glTranslate (...), chacune étant multipliée par la matrice actuelle. Étant donné que ces calculs ne seront effectués qu'une fois par image, il n'y a pas d'exigences particulières de vitesse, vous pouvez le faire avec des flotteurs simples, sans perversions avec des nombres à virgule fixe. La matrice elle-même est un tableau de float [4] [4], mappé sur un tableau unidimensionnel de float [16] - en fait, cette méthode est généralement utilisée pour les tableaux dynamiques, mais vous pouvez tirer un petit avantage des statiques. Un autre hack standard: au lieu de calculer constamment les sinus et les cosinus, qui sont nombreux dans les matrices de rotation,comptez-les à l'avance et écrivez-les sur la tablette. Pour ce faire, divisez le cercle complet en 256 parties, calculez la valeur du sinus pour chacune et transférez-la dans le tableau sin_table []. Eh bien, n'importe qui de l'école peut obtenir le cosinus du sinus. Il convient de noter que les fonctions de rotation prennent un angle non pas en radians, mais en fractions de tour complet, après réduction à la plage [0 ... 255]. Cependant, des fonctions «honnêtes» ont été mises en œuvre pour effectuer la conversion de l'angle en lobes sous le capot.effectuer la conversion de l'angle en lobes sous le capot.effectuer la conversion de l'angle en lobes sous le capot.

Lorsque la matrice est prête, vous pouvez commencer à dessiner les primitives. En général, dans les graphiques en trois dimensions, il existe trois types de primitives - un point, une ligne et un triangle. Mais si nous nous intéressons aux modèles polygonaux, il ne faut s'intéresser qu'au triangle. Son "rendu" se produit dans la fonction glDrawTriangle () ou glDrawTriangleV (). Le mot «rendu» est placé entre guillemets car aucun rendu ne se produit à ce stade. Nous multiplions simplement tous les points de la primitive par la matrice de transformation, puis nous en extrayons les formules analytiques des arêtes y = ky * x + par, ce qui nous permet de trouver les intersections des trois arêtes du triangle avec la ligne de sortie actuelle. Nous en écartons un, car il ne repose pas sur l'intervalle entre les sommets, mais sur sa continuation.Autrement dit, pour dessiner un cadre, il vous suffit de parcourir toutes les lignes et pour chaque peinture la zone entre les points d'intersection. Mais si vous appliquez cet algorithme de front, chaque primitive chevauchera celles qui ont été dessinées précédemment. Nous devons considérer la coordonnée Z (profondeur) afin que les triangles se croisent magnifiquement. Au lieu d'imprimer simplement point par point, nous considérerons sa coordonnée Z et, en comparaison avec la coordonnée Z stockée dans le tampon de profondeur, soit en sortie (en mettant à jour le tampon Z) soit en l'ignorant. Et pour calculer la coordonnée Z de chaque point de la ligne qui nous intéresse, nous utilisons la même formule de ligne droite z = kz * y + bz calculée par les deux mêmes points d'intersection avec des bords. Par conséquent, l'objet du triangle "semi-fini" struct glTriangle se compose de trois coordonnées X des sommets (il n'y a aucun sens à stocker les coordonnées Y et Z, elles seront calculées) et k,b coefficients directs, eh bien, couleur au tas. Ici, contrairement au calcul des matrices de transformation, la vitesse est critique, nous utilisons donc déjà des nombres à virgule fixe. De plus, si pour le terme b, la même précision est suffisante que pour les coordonnées (2 octets), alors la précision du facteur k, plus grande est la meilleure, on prend donc 4 octets. Mais pas un flottant, car travailler avec des entiers est encore plus rapide, même avec la même taille.

Ainsi, en appelant un groupe de glDrawTriangle (), nous avons préparé un tableau de triangles semi-finis. Dans mon implémentation, les triangles sont déduits un à la fois par des appels de fonction explicites. En fait, il serait logique d'avoir un tableau de triangles avec les adresses des sommets, mais ici j'ai décidé de ne pas compliquer. Quoi qu'il en soit, la fonction de rendu est écrite par des robots et peu leur importe de remplir un tableau constant ou d'écrire trois cents appels identiques. Il est temps de traduire les produits semi-finis des triangles en une belle image à l'écran. Pour ce faire, la fonction glSwapBuffers () est appelée. Comme décrit ci-dessus, il parcourt les lignes de l'affichage, recherche chaque point d'intersection avec tous les triangles et dessine des segments en fonction du filtrage par profondeur. Après avoir rendu chaque ligne, vous devez envoyer cette ligne à l'écran. Pour ce faire, DMA est lancé, ce qui indique l'adresse de la chaîne et sa taille.En attendant, DMA fonctionne, vous pouvez basculer vers un autre tampon et afficher la ligne suivante. L'essentiel est de ne pas oublier d'attendre la fin du transfert si vous avez soudainement terminé le rendu plus tôt. Pour visualiser le rapport des vitesses, j'ai ajouté l'inclusion d'une LED rouge après la fin du rendu et éteinte après la fin de l'attente DMA. Il s'avère quelque chose comme PWM, qui ajuste la luminosité en fonction de la latence. Théoriquement, au lieu d'une attente «stupide», des interruptions DMA pourraient être utilisées, mais je ne pourrais pas les utiliser, et l'algorithme serait devenu beaucoup plus compliqué. Pour un programme de démonstration, c'est redondant.Pour visualiser le rapport des vitesses, j'ai ajouté l'inclusion d'une LED rouge après la fin du rendu et éteinte après la fin de l'attente DMA. Il s'avère quelque chose comme PWM, qui ajuste la luminosité en fonction de la latence. Théoriquement, au lieu d'une attente «stupide», des interruptions DMA pourraient être utilisées, mais je ne pourrais pas les utiliser, et l'algorithme serait devenu beaucoup plus compliqué. Pour un programme de démonstration, c'est redondant.Pour visualiser le rapport des vitesses, j'ai ajouté l'inclusion d'une LED rouge après la fin du rendu et éteinte après la fin de l'attente DMA. Il s'avère quelque chose comme PWM, qui ajuste la luminosité en fonction de la latence. Théoriquement, au lieu d'une attente «stupide», des interruptions DMA pourraient être utilisées, mais je ne pourrais pas les utiliser, et l'algorithme serait devenu beaucoup plus compliqué. Pour un programme de démonstration, c'est redondant.

Le résultat des procédures ci-dessus a été une image tournante de trois plans qui se croisent de couleurs différentes, et avec une vitesse assez décente: la luminosité de la LED rouge est assez élevée, ce qui indique une grande marge dans les performances du noyau.

Eh bien, si le noyau est inactif, vous devez le charger. Et nous le chargerons avec de meilleurs modèles. Cependant, n'oubliez pas que la mémoire est encore très limitée, donc le contrôleur ne tirera pas trop de polygones physiquement. Le calcul le plus simple a montré qu'après soustraction de la mémoire sur le tampon de ligne et autres, il y avait une place pour 378 triangles. Comme la pratique l'a montré, les modèles de l'ancien mais intéressant jeu gothique sont parfaits pour cette taille. En fait, les modèles d'un serpent et d'une mouche de sang ont été retirés de là (et déjà au moment de la rédaction de cet article et d'un glocoor, affichant sur KDPV), après quoi le contrôleur a manqué de mémoire flash. Mais les modèles de jeux ne sont pas destinés à être utilisés par un microcontrôleur.

Disons qu'ils contiennent des animations, des textures et similaires, ce qui ne nous est pas utile et ne tient pas en mémoire. Heureusement, Blender permet non seulement de les enregistrer dans * .obj, ce qui est plus adapté à l'analyse, mais également de réduire le nombre de polygones si nécessaire. De plus, à l'aide d'un simple programme auto-écrit obj2arr * .obj, les fichiers sont triés en coordonnées, à partir desquelles un fichier * .h est ensuite formé pour une inclusion directe dans le firmware.

Mais pour l'instant, les modèles ressemblent à de simples taches bouclées. Sur le modèle de test, cela ne nous a pas dérangés, car tous les visages ont été peints dans leurs propres couleurs, mais ne prescrivent pas les mêmes couleurs à chaque polygone du modèle. Non, vous pouvez, bien sûr, peindre une mouche dans des couleurs aléatoires, mais elle aura l'air assez à l'improviste, j'ai vérifié. Surtout lorsque les couleurs changent également sur chaque image ... Au lieu de cela, appliquez une autre goutte de magie vectorielle et ajoutez de l'éclairage.

Le calcul de l'éclairage dans sa version primitive consiste à calculer le produit scalaire de la normale et de la direction de la source lumineuse, puis à multiplier par la couleur «native» du visage.
Nous avons maintenant trois modèles - deux du jeu et un test, à partir duquel nous avons commencé. Pour les commuter, nous utiliserons l'un des deux boutons soudés sur la carte. En même temps, vous pouvez ajouter un contrôle sur le processeur. Nous avons déjà un contrôle - une LED rouge associée à la latence DMA. Et la deuxième, verte, LED, nous clignotera à chaque mise à jour de trame - afin que nous puissions estimer la fréquence d'images. Pour l'œil nu, c'était environ 15 fps.


En général, je suis satisfait du résultat: c'est bien d'implémenter quelque chose qui est fondamentalement impossible à résoudre de front. Bien sûr, il reste encore beaucoup à optimiser et à améliorer, mais cela ne sert à rien. Objectivement, le contrôleur pour les graphiques en trois dimensions est faible, et ce n'est même pas la vitesse, mais plutôt la RAM. Cependant, comme tout échantillon de demoscene, ce projet est précieux non pas par le résultat, mais par le processus.

Si quelqu'un est soudain intéressé, le code source est disponible ici .

All Articles