
Je suis le propriétaire d'un merveilleux appareil - enregistreur GPS Holux M-241. La chose est très pratique et utile en voyage. Avec l'aide d'un enregistreur, j'écris une trace GPS d'un voyage, le long de laquelle vous pouvez ensuite voir votre chemin en détail, et attacher également les photos que vous prenez aux coordonnées GPS. Il a également un petit écran qui affiche des informations supplémentaires - heures, vitesse actuelle, altitude et direction, odomètre et bien plus encore.
Ici, j'ai écrit une fois une courte critique.
Avec tous les avantages d'un morceau de fer, j'ai commencé à en sortir. Il me manque quelques petites mais utiles: quelques odomètres, montrant la vitesse verticale, mesurant les paramètres d'une section de piste. Cela semble être de petites choses, mais la société Holux a trouvé cela pas assez utile pour la mise en œuvre dans le firmware. De plus, je n'aime pas certains paramètres du matériel, et certaines choses sont devenues obsolètes en 10 ans ...
À un moment donné, j'ai réalisé que je peux moi-même créer un enregistreur avec les fonctionnalités dont j'ai besoin. Heureusement, tous les composants nécessaires sont assez bon marché et abordables. J'ai commencé à faire mon implémentation basée sur Arduino. Sous la coupe, un journal de construction où j'ai essayé de peindre mes solutions techniques.
Définition des fonctionnalités
Beaucoup demanderont pourquoi j'ai besoin de construire mon propre enregistreur, s'il est certain qu'il y a quelque chose de prêt pour d'éminents fabricants. C'est possible. Pour être honnête, je ne l'ai pas vraiment cherché. Mais c'est sûr qu'il manquera quelque chose. En tout cas, ce projet est un fan pour moi. Pourquoi ne commençons-nous pas à construire notre appareil de rêve?
Donc, pour ce que j'apprécie mon Holux M-241.
- L'écran fait une «boîte noire», dont les résultats ne sont disponibles qu'après le voyage, un outil très pratique, dont les lectures sont disponibles ici et maintenant. Avoir un écran rend possible presque toutes les fonctionnalités de cette liste.
- Une montre est utile en soi. Lors de voyages GPS, l'enregistreur suspendu à une ficelle autour de son cou se révèle souvent être plus proche qu'un téléphone portable dans sa poche ou dans un sac à dos. La montre prend en charge tous les fuseaux horaires (mais avec commutation manuelle)
- Le bouton POI vous permet de marquer les coordonnées actuelles sur la piste. Par exemple, pour noter un point de repère qui s'est glissé à l'extérieur de la fenêtre du bus, à propos duquel je veux google plus tard.
- À l'aide de l' odomètre, vous pouvez mesurer la distance parcourue à partir d'un certain point. Par exemple, la distance parcourue par jour ou la longueur d'une piste.
- La vitesse, l'altitude et la direction actuelles vous aident à vous retrouver dans l'espace
- La capacité de survie de 12 à 14 heures d'une pile AA dans la plupart des cas vous permet de ne pas penser aux problèmes d'alimentation. C'est-à-dire charge presque toujours suffisante pour une journée complète de voyage.
- Compact et facile à utiliser - les choses dans le monde moderne sont très belles
Cependant, certaines choses pourraient être légèrement améliorées:
- Le sous-système d'alimentation des piles AA est considéré par beaucoup comme un avantage certain - une batterie dure longtemps et vous pouvez reconstituer l'alimentation dans n'importe quel désert. Vous pouvez vous approvisionner pendant au moins un mois de camping autonome.
Mais pour moi, la durée de vie de la batterie est pure hémorroïdes. Vous devez transporter une poignée de piles et qui sait à quel point elles sont de haute qualité (soudain, elles étaient allongées sur une étagère pendant 5 ans et déjà auto-déchargées). Avec les batteries, l'hémorragie est encore plus importante. Mon chargeur ne peut se charger que par paire. Nous devons décharger les batteries afin qu'elles soient du même degré de décharge. Par conséquent, vous ne vous souvenez jamais où vous avez déjà été libéré, et où pas encore.
Pendant 6 ans d'utilisation de l'enregistreur, je ne me suis retrouvé dans le désert sans électricité qu'à quelques reprises. En règle générale, j'ai accès au point de vente au moins une fois par jour. Dans ce cas, la batterie au lithium intégrée serait beaucoup plus pratique. Eh bien, dans les cas extrêmes, j'ai une banque de pavés.
- L'indication du degré de décharge est faite très stupidement - l'indicateur commence à clignoter lorsque la batterie est sur le point de se décharger. De plus, il peut mourir en 5 minutes et peut-être travailler encore une heure. Il est très facile de manquer ce moment et de perdre une partie du journal.
- En tant que personne intéressée par l'aviation, il serait très intéressant pour moi d'observer la vitesse verticale actuelle .
- Quelques odomètres - il est souvent intéressant de mesurer plus d'une distance. Par exemple, la distance parcourue par jour et pour tout le trajet.
- L'odomètre se réinitialise lorsque vous éteignez l'appareil ou lorsque vous remplacez la batterie. C'est terriblement inconfortable. Si vous vous êtes arrêté pour un repas dans un café, l'enregistreur GPS ne peut pas être éteint car la valeur sera réinitialisée. Il doit le laisser allumé et il continue à parcourir des kilomètres et à manger la batterie. Il serait beaucoup plus pratique de pouvoir mettre l'odomètre en pause et de sauvegarder les valeurs entre les inclusions.
- Mesure des paramètres du site . En ski par exemple, je m'intéresse à la durée de la descente, à l'altitude, à la vitesse moyenne et maximale sur le site, au temps passé. Ce que vous voulez savoir, c'est tout de suite, et pas à la maison lorsque vous téléchargez la piste.
- La précision est médiocre. Lorsque vous vous déplacez rapidement - rien d'autre. Mais lorsque la vitesse est faible sur la piste, des «bruits» + - 50m sont clairement visibles. Et pendant une heure de repos, vous pouvez «insister» sur près d'un kilomètre. L'avantage de la technologie depuis 10 ans est allé de l'avant et les récepteurs modernes offrent une précision beaucoup plus grande.
- La vitesse de fusion des pistes n'est que de 38 400. Non, eh bien, ce n'est pas grave en 2017 d'utiliser le port COM pour transférer de grandes quantités de données. La fusion de 2 mégaoctets de flash interne prend plus de 20 minutes.
De plus, tous les programmes ne peuvent pas digérer le format des pistes fusionnées. L'utilitaire natif est très misérable. Heureusement, il existe le BT747, qui peut fusionner correctement la piste et la convertir en une sorte de format digestible.
- La taille du lecteur flash n'est que de 2 Mo. D'une part, cela suffit pour un voyage de deux semaines avec des points d'économie toutes les 5 secondes. Mais d'abord, le format interne emballé
nécessite une conversion, et d'autre part ne permet pas d'augmenter le volume - Le périphérique de stockage de masse pour une raison quelconque n'est plus à la mode. Les interfaces modernes tentent de masquer le fait de la présence de fichiers. Je travaille avec les ordinateurs depuis 25 ans, et travailler directement avec des fichiers est beaucoup plus pratique pour moi que de toute autre manière.
Il n'y a rien ici qui ne pourrait être réalisé sans efforts importants.
Rien de différent. Je ne l'utilise pas moi-même, mais soudain, quelqu'un est utile:
- Affiche les coordonnées actuelles (latitude, longitude)
- Différentes icônes sont dessinées sur le côté gauche de l'écran, dont je ne me souviens même pas de l'essence sans manuel.
- Il y a des changements de mètres / km - pieds / miles.
- Bluetooth - l'enregistreur peut être connecté à des téléphones mobiles sans GPS.
- La distance absolue au point.
- Enregistrement par temps (toutes les N secondes) ou par distance (tous les X mètres).
- Prise en charge de différentes langues.
Choisissez le fer
Les exigences sont plus ou moins définies. Il est temps de comprendre comment tout cela peut être mis en œuvre. Les principaux composants que j'aurai seront:
- Microcontrôleur - Je n'ai aucun plan pour des algorithmes de calcul sophistiqués, donc la puissance de traitement du noyau n'est pas particulièrement importante. Je n'ai pas non plus d'exigences particulières pour le remplissage - un ensemble de périphériques standard fera l'affaire.
À portée de main était juste une dispersion de divers arduinoes, ainsi que quelques stm32f103c8t6. J'ai décidé de commencer par AVR, que je connais bien au niveau du contrôleur / registres / périphériques. Si je rencontre des restrictions - il y aura une raison de ressentir le STM32.
- Le récepteur GPS a été sélectionné parmi les modules NEO6MV2, Beitan BN-800 et Beitan BN-880. Forums googlé pendant un certain temps. Des gens expérimentés ont dit que le premier récepteur est le siècle dernier. Les deux autres ne diffèrent que par l'emplacement de l'antenne - dans le BN-800, il est suspendu au fil, et dans le BN-880, il est collé avec un sandwich au module principal. A pris un BN-880 .
- Écran - l'original utilise un écran LCD 128 x 32 avec rétro-éclairage. Je n'ai pas trouvé exactement la même chose. J'ai acheté un OLED 0,91 "sur le contrôleur SSD1306 et un écran LCD 1,2" sur le contrôleur ST7565R . J'ai décidé de partir du premier, car il est plus facile de se connecter avec un peigne standard selon I2C ou SPI. Mais il est légèrement plus petit par rapport à l'original, et il ne fonctionnera pas non plus pour afficher constamment l'image pour des raisons d'efficacité énergétique. Le deuxième écran devrait être moins gourmand, mais vous devez lui souder un connecteur délicat et comprendre comment alimenter le rétro-éclairage.
Des petites choses:
- Les boutons ont une fois acheté un sac entier;
- Bouclier avec pour carte SD - également à portée de main;
- J'ai acheté une paire de contrôleurs de charge différents pour les batteries au lithium, mais je ne le comprenais toujours pas.
J'ai décidé de concevoir la carte à la toute fin, lorsque le firmware est prêt. À ce moment, je déciderai enfin des principaux composants et du schéma de leur inclusion. À la première étape, j'ai décidé de faire le débogage sur la maquette en connectant les composants à l'aide de cordons de brassage.
Mais vous devez d'abord décider d'une question très importante - la nutrition des composants. Il m'a semblé raisonnable d'alimenter tout à partir de 3,3 V: le GPS et l'écran uniquement dessus et de savoir comment travailler. Il s'agit également de la tension native pour USB et SD. De plus, le circuit peut être alimenté à partir d'une boîte de lithium.
Le choix s'est porté sur l'Arduino Pro Mini, que l'on retrouve dans la version 8MHz / 3,3V. Mais elle n'avait pas d'USB à bord - j'ai dû utiliser un adaptateur USB-UART.
Premiers pas
Initialement, le projet a été créé dans Arduino IDE. Mais pour être honnête, ma langue n'ose pas l'appeler un IDE - comme un éditeur de texte avec un compilateur. En tout cas, après Visual Studio, dans lequel je travaille depuis 13 ans, je ne peux rien faire de sérieux dans l'IDE Arduino sans larmes et matyuk.
Heureusement, il existe un Atmel Studio gratuit, dans lequel même Visual Assist est intégré dès la sortie de la boîte !!! Le programme sait tout ce qui est nécessaire, tout est familier et à sa place. Eh bien, presque tout (je n'ai pas trouvé comment compiler un seul fichier, par exemple, pour vérifier la syntaxe)

Commencé à partir de l'écran - cela est nécessaire pour déboguer le squelette du firmware, puis le remplir de fonctionnalités. Il s'est arrêté à la
première bibliothèque disponible pour Adafruit SSD1306 . Elle sait tout ce qui est nécessaire et fournit une interface très simple.
Joué avec des polices. Il s’est avéré qu’une police peut prendre jusqu’à 8 Ko (la taille des lettres est de 24 pt) - vous ne pouvez surtout pas vous déplacer dans un contrôleur de 32 Ko. De grandes polices sont nécessaires, par exemple, pour afficher l'heure.
Exemple de code de police#include <Adafruit_GFX.h> #include <Adafruit_SSD1306.h> #include <gfxfont.h> #include <fonts/FreeMono12pt7b.h> #include <fonts/FreeMono18pt7b.h> ... #include <fonts/FreeSerifItalic24pt7b.h> #include <fonts/FreeSerifItalic9pt7b.h> #include <fonts/TomThumb.h> struct font_and_name { const char * PROGMEM name; GFXfont * font; }; #define FONT(name) {#name, &name} const font_and_name fonts[] = { // FONT(FreeMono12pt7b), FONT(FreeMono18pt7b), /* FONT(FreeMono24pt7b), FONT(FreeMono9pt7b), FONT(FreeMonoBold12pt7b), ... FONT(FreeSerifItalic9pt7b), FONT(TomThumb)*/ }; const unsigned int fonts_count = sizeof(fonts) / sizeof(font_and_name); unsigned int current_font = 0; extern Adafruit_SSD1306 display; void RunFontTest() { display.clearDisplay(); display.setCursor(0,30); display.setFont(fonts[current_font].font); display.print("12:34:56"); display.setCursor(0,6); display.setFont(&TomThumb); display.print(fonts[current_font].name); display.display(); } void SwitchToNextFont() { current_font = ++current_font % fonts_count; }
Les polices complètes avec la bibliothèque sont très maladroites. La police monospace s'est avérée très large - la ligne «12:34:56» ne convient pas, Serif - tous les nombres sont de poids différents. À moins que la police standard 5x7 de la bibliothèque ne semble comestible.


Il s'est avéré que ces polices ont été converties à partir de certaines polices ttf open source qui ne sont tout simplement pas optimisées pour les petites résolutions.
J'ai dû dessiner mes polices. Plus précisément, commencez par déterrer les symboles individuels des symboles finis. Le symbole «:» dans le tableau ASCII est très utile juste après les chiffres et peut être acheté en un seul bloc. Il est également pratique de créer une police non pas pour tous les caractères, mais uniquement pour une plage, par exemple, de 0x30 ('0') à 0x3a (':'). T.O. de FreeSans18pt7b, il s'est avéré faire une police très compacte uniquement pour les caractères nécessaires. Certes, j'ai dû ajuster légèrement la largeur pour que le texte tienne dans la largeur de l'écran.
Sous-police FreeSans18pt7b Il s'est avéré que la police 18pt avait en fait 25 pixels de haut. Pour cette raison, il tient légèrement sur une autre inscription

L'affichage inversé, soit dit en passant, aide à comprendre où se trouvent réellement les limites de la zone de dessin et comment se situe la ligne par rapport à cette bordure - l'affichage a de très grands cadres.
Googlé pendant longtemps les polices prêtes à l'emploi, mais elles ne correspondaient ni à la taille, ni à la forme, ni au contenu. Par exemple, sur Internet, un arbre de polices 8x12 (vidages de générateurs de caractères de carte VGA). Mais en fait, ces polices sont 6x8, c'est-à-dire beaucoup de promenades dans l'espace - dans le cas d'une résolution et d'une taille aussi petites que la mienne, elles sont essentielles.
J'ai dû dessiner mes propres polices, car le format de police de la bibliothèque Adafruit est très simple. J'ai préparé l'image dans Paint.net - j'ai simplement dessiné les lettres dans la bonne police, puis je les ai corrigées un peu avec un crayon. J'ai enregistré l'image au format png, puis je l'ai envoyée rapidement au script python écrit sur mon genou. Ce script a généré un code semi-fini qui règle déjà point par point dans l'EDI directement dans les codes hexadécimaux.

Par exemple, voici à quoi ressemble le processus de création d'une police à espacement fixe 8x12 avec de petites lettres et un interligne. À la fin, chaque caractère s'est avéré être d'environ 7 x 10 et, par défaut, il occupait 10 octets. Il serait possible d'emballer chaque caractère dans 8-9 octets (la bibliothèque le permet), mais je n'ai pas pris la peine. De plus, dans ce formulaire, vous pouvez modifier des pixels individuels directement dans le code.
Cadre
L'appareil d'origine fournit une interface très simple et pratique. Les informations sont regroupées en catégories qui sont affichées à partir de pages individuelles (écrans). À l'aide du bouton, vous pouvez parcourir les pages et utiliser le deuxième bouton pour sélectionner l'élément en cours ou effectuer l'action indiquée dans la signature sous le bouton. Cette approche me semble très pratique et il n'y a rien à changer.
J'aime la beauté de la POO, car j'ai immédiatement ébloui une petite interface, chaque page implémente l'interface comme elle l'exige. La page sait se dessiner et met en œuvre la réaction aux boutons.
class Screen { Screen * nextScreen; public: Screen(); virtual ~Screen() {} virtual void drawScreen() = 0; virtual void drawHeader(); virtual void onSelButton(); virtual void onOkButton(); virtual PROGMEM const char * getSelButtonText(); virtual PROGMEM const char * getOkButtonText(); Screen * addScreen(Screen * screen); };
Les boutons peuvent effectuer diverses actions en fonction de l'écran actuel. Par conséquent, le haut de l'écran avec une hauteur de 8 pixels, j'ai attribué aux étiquettes pour les boutons. Le texte des signatures dépend de l'écran actuel et est retourné par les fonctions virtuelles getSelButtonText () et getOkButtonText (). Également dans l'en-tête, les éléments de service tels que la force du signal GPS et la charge de la batterie seront toujours affichés. Les écrans remaining restants sont disponibles pour des informations utiles.
Comme je l'ai dit, les écrans peuvent être retournés, ce qui signifie qu'il devrait y avoir quelque part une liste d'objets pour différentes pages. À quoi plusieurs écrans peuvent être imbriqués, comme un sous-menu. J'ai même commencé la classe ScreenManager, qui était censée gérer ces listes, mais j'ai ensuite trouvé la solution plus facile.
Ainsi, chaque écran a simplement un pointeur sur le suivant. Si l'écran vous permet d'accéder au sous-menu, il ajoute un pointeur de plus à l'écran de ce sous-menu
class Screen { Screen * nextScreen; … }; class ParentScreen : public Screen { Screen * childScreen; … };
Par défaut, le gestionnaire de boutons appelle simplement la fonction de changement d'écran, en lui passant le pointeur souhaité. La fonction s'est avérée être triviale - elle vient de basculer le pointeur sur l'écran actuel. Pour assurer l'imbrication des écrans, j'ai fait une petite pile. Ainsi, l'ensemble du gestionnaire d'écran tient dans 25 lignes et 4 petites fonctions.
Screen * screenStack[3]; int screenIdx = 0; void setCurrentScreen(Screen * screen) { screenStack[screenIdx] = screen; } Screen * getCurrentScreen() { return screenStack[screenIdx]; } void enterChildScreen(Screen * screen) { screenIdx++;
Certes, le code pour remplir ces structures n'est pas très joli, mais jusqu'à présent, il n'a pas été mieux inventé.
Screen * createCurrentTimeScreen() { TimeZoneScreen * tzScreen = new TimeZoneScreen(1, 30); tzScreen = tzScreen->addScreen(new TimeZoneScreen(2, 45)); tzScreen = tzScreen->addScreen(new TimeZoneScreen(-3, 30));
La penséeLa structuration, bien sûr, s'est avérée belle, mais je crains qu'elle ne mange beaucoup de mémoire. Vous devez aller contre vous-même et zafigachit une grande table statique avec des pointeurs.
Allez-y. Dans mon implémentation de l'interface, je voulais faire quelque chose comme une boîte de message, un court message qui apparaîtrait pendant une seconde ou deux, puis disparaîtrait. Par exemple, si vous appuyez sur le bouton POI (Point Of Interest) sur l'écran avec les coordonnées actuelles, puis en plus d'écrire le point sur la piste, il serait bien de montrer à l'utilisateur le message «Waypoint Saved» (dans l'appareil d'origine, une icône supplémentaire ne s'affiche que pendant une seconde). Ou, lorsque la batterie est faible, «remontez le moral» de l'utilisateur avec un message.

Étant donné que les données du GPS viendront constamment, il ne peut être question d'aucune fonction de blocage. Par conséquent, j'ai dû inventer une machine à états simple (machine à états), qui dans la fonction loop () choisirait quoi faire - afficher l'écran ou la boîte de message actuelle.
enum State { IDLE_DISPLAY_OFF, IDLE, MESSAGE_BOX, BUTTON_PRESSED, };
Il est également pratique de gérer les pressions de bouton à l'aide de la machine d'état. Peut-être que ce serait correct à travers des interruptions, mais ça s'est bien passé aussi. Cela fonctionne comme ceci: si un bouton a été enfoncé à l'état IDLE, rappelez-vous l'heure à laquelle il a été enfoncé et passez à l'état BUTTON_PRESSED. Dans cet état, nous attendons que l'utilisateur relâche le bouton. Ici, nous pouvons calculer la durée lorsque le bouton a été enfoncé. Les réponses courtes (<30 ms) sont simplement ignorées - il s'agit très probablement d'un rebond de contacts. Les longs trajets peuvent déjà être interprétés comme une pression sur un bouton.
Je prévois d'utiliser à la fois des pressions courtes sur les boutons pour les actions ordinaires et des pressions longues (> 1c) pour les fonctions spéciales. Par exemple, une pression courte démarre / met en pause le compteur kilométrique, une pression longue réinitialise le compteur à 0.
Peut-être que d'autres États seront ajoutés. Ainsi, par exemple, dans l'enregistreur d'origine après le passage à la page suivante, les valeurs à l'écran changent souvent, et moins souvent après quelques secondes - une fois par seconde. Cela peut être fait en ajoutant un autre état.
Lorsque le cadre était prêt, j'ai déjà commencé à connecter le GPS. Mais ici, il y avait des nuances qui m'ont fait reporter cette tâche.
Optimisation du firmware
Avant de continuer, je dois me laisser distraire par certains détails techniques. Le fait est qu'à peu près à cet endroit, je commençai à augmenter ma consommation de mémoire. Il s'est avéré que la ligne déclarée imprudemment sans le modificateur PROGMEM au début du firmware est copiée dans la RAM et y occupe de l'espace tout au long de l'exécution.
Architectures diversesEn un mot. Sur les gros ordinateurs, l'
architecture Von Neumann est utilisée lorsque le code et les données sont situés dans le même espace d'adressage. C'est-à-dire les données de la RAM et de la ROM seront lues de la même manière.
Les microcontrôleurs utilisent généralement
l'architecture Harvard , où le code et les données sont séparés. T.O. vous devez utiliser diverses fonctions pour lire la mémoire et le flash. Du point de vue du langage C / C ++, les pointeurs se ressemblent, mais lors de l'écriture d'un programme, nous devons savoir exactement où exactement vers quelle mémoire pointe notre pointeur et appeler les fonctions correspondantes.
Heureusement, les développeurs de bibliothèques se sont déjà partiellement occupés de cela. La classe principale de la bibliothèque d'affichage - Adafruit_SSD1306 est héritée de la classe Print de la bibliothèque standard Arduino.
Cela nous fournit toute une série de différentes modifications de la méthode d'impression - pour imprimer des chaînes, des caractères uniques, des chiffres et autre chose. Il a donc 2 fonctions distinctes pour l'impression des lignes:
size_t print(const __FlashStringHelper *); size_t print(const char[]);
Le premier sait que vous devez imprimer une ligne à partir d'un lecteur flash et la charge caractère par caractère. Le second imprime des caractères à partir de la RAM. En fait, ces deux fonctions prennent un pointeur sur une chaîne, uniquement à partir d'espaces d'adressage différents.
Pendant longtemps, j'ai cherché dans le code arduino ce très __FlashStringHelper pour apprendre à appeler la fonction print () souhaitée. Il s'est avéré que les gars ont fait l'affaire: ils ont simplement déclaré ce type avec la déclaration directe (sans déclarer le type lui-même) et ont écrit une macro qui a casté des pointeurs vers des lignes en un éclair vers le type __FlashStringHelper. Juste pour que le compilateur sélectionne la fonction surchargée nécessaire
class __FlashStringHelper; #define F(string_literal) (reinterpret_cast<const __FlashStringHelper *>(PSTR(string_literal)))
Cela vous permet d'écrire comme ceci:
display.print(F(“String in flash memory”));
Mais ne vous laisse pas écrire comme ça
const char text[] PROGMEM = "String in flash memory"; display.print(F(text));
Et, apparemment, la bibliothèque ne fournit rien de ce qui pourrait être fait de cette façon. Je sais qu'il n'est pas bon d'utiliser des éléments de bibliothèque privée dans mon code, mais que dois-je faire? J'ai dessiné ma macro, qui a fait ce dont j'avais besoin.
#define USE_PGM_STRING(x) reinterpret_cast<const __FlashStringHelper *>(x)
La fonction de dessin du chapeau a donc commencé à ressembler à ceci:
void Screen::drawHeader() { display.setFont(NULL); display.setCursor(20, 0); display.print('\x1e'); display.print(USE_PGM_STRING(getSelButtonText())); display.setCursor(80, 0); display.print('\x1e'); display.print(USE_PGM_STRING(getOkButtonText())); }
Eh bien, depuis que je suis entré dans les éléments de bas niveau du firmware, j'ai décidé d'étudier plus en détail comment tout cela fonctionne à l'intérieur.
En général, les gars qui ont créé Arduino doivent ériger un monument. Ils ont créé une plate-forme simple et pratique pour le prototypage et l'artisanat. Un grand nombre de personnes ayant une connaissance minimale de l'électronique et de la programmation ont pu entrer dans le monde d'Arduino. Mais tout cela est lisse et beau tout en faisant des ordures comme des oeillères avec des LED ou en lisant le thermomètre. Dès que vous vous penchez sur quelque chose de sérieux, vous devez immédiatement comprendre plus profondément que vous ne le vouliez depuis le début.
Ainsi, après chaque bibliothèque ou même classe ajoutée, j'ai noté la vitesse à laquelle la consommation de mémoire augmente. À ce stade, j'étais occupé avec plus de 14 Ko de 32 Ko de mémoire flash et 1 300 octets de RAM (sur 2 Ko). Chaque mouvement imprudent a ajouté 10% de plus à celui déjà utilisé. Mais je n'ai toujours pas vraiment connecté les bibliothèques GPS et SD / FAT32, et le chat lui-même pleurait. J'ai dû prendre le
vérificateur de désassembleur et étudier ce que le compilateur a fait.
J'espérais secrètement que l'éditeur de liens supprimait les fonctions inutilisées. Mais il s'est avéré que certains d'entre eux étaient insérés presque entièrement. Dans le firmware, j'ai trouvé les fonctions de dessin au trait et quelques autres de la bibliothèque de travailler avec l'écran, bien que dans le code, je ne les appelais évidemment pas à ce moment-là. Implicitement, ils ne devraient pas être appelés non plus - pourquoi ai-je besoin d'une fonction de dessin au trait si je ne dessine que des lettres à partir de bitmaps? Plus de 5,2 Ko à l'improviste (et c'est sans compter les polices).
En plus de la bibliothèque de contrôle d'affichage, j'ai également trouvé:
- 2,6 ko - sur SoftwareSerial (je l'ai intégré dans le projet à un moment donné)
- 1,6 ko - I2C
- 1,3 ko - HardwareSerial
- 2 ko - TinyGPS
- 2,5 ko sur l'arduino réel (initialisation, pins, toutes sortes de tableaux, le timer principal pour les fonctions millis () et delay ()),
Les chiffres sont très indicatifs, comme l'optimiseur mélange sérieusement le code. Certaines fonctions peuvent démarrer à un endroit, puis une autre à partir d'une autre bibliothèque, appelée depuis la première, peut immédiatement la suivre. De plus, des branches distinctes de ces fonctions peuvent être situées à l'autre extrémité du flash.
Aussi dans le code que j'ai trouvé:
- Contrôle d'écran par SPI (bien que je l'ai connecté via I2C)
- Méthodes de classes de base qui elles-mêmes ne sont pas appelées, car redéfini dans les héritiers
- Des destructeurs qui ne sont jamais appelés par conception
- Fonctions de dessin (et pas toutes - une partie des fonctions que l'éditeur de liens lançait toujours)
- malloc / free alors que dans mon code tous les objets sont essentiellement statiques
Mais non seulement la consommation de mémoire flash, mais aussi la SRAM croît à pas de géant:
- 130 octets - I2C
- 100 octets - SoftwareSerial
- 157 octets - Série
- 558 octets - Affichage (dont 512 est le tampon de trame)
La section .data n'était pas moins divertissante. Il y a environ 700 octets et cette chose est chargée à partir d'un flash dans la RAM au début. Il s'est avéré qu'il existe des emplacements réservés pour les variables en mémoire, ainsi que les valeurs d'initialisation. Ici vivent les variables et les constantes que vous avez oublié de déclarer comme const PROGMEM.
Parmi cela, il y avait un tableau lourd avec un «écran de démarrage» de l'écran - les valeurs initiales du tampon de trame. Théoriquement, si vous faites l'écran display () immédiatement après le début, vous pouvez voir la fleur et l'inscription Adafruit, mais dans mon cas, il est inutile de dépenser de la mémoire flash à ce sujet.
La section .data contient également des vtables. Ils sont copiés en mémoire à partir d'un lecteur flash, apparemment pour des raisons d'efficacité lors de l'exécution. Mais vous devez sacrifier un assez gros morceau de RAM - plus d'une douzaine de classes plus de 150 octets. De plus, il semble qu'il n'y ait pas de clé de compilation qui, sacrifiant les performances, laisse les tables virtuelles dans la mémoire flash.
Que faire à ce sujet? Je ne sais pas encore. Cela dépendra de la façon dont la consommation continuera de croître. Pour de bons montants trouvés doivent être réparés sans pitié. Selon toute vraisemblance, je devrai attirer toutes les bibliothèques dans mon projet de manière explicite, puis les couvrir complètement. Et vous devrez peut-être également réécrire certaines des pièces différemment afin d'optimiser la mémoire. Ou passez à un matériel plus puissant. En tout cas, maintenant je connais le problème et il existe une stratégie pour le résoudre.
MISE À JOUR:Peu de progrès dans l'utilisation efficace des ressources. Je fais une mise à jour de cette partie, car dans le prochain je veux me concentrer sur des choses complètement différentes.
Dans les commentaires, il y a une certaine confusion quant à l'utilisation de C ++. En particulier, pourquoi est-il si mauvais et conserve-t-il sa table dans une précieuse RAM? En général, les fonctions, constructeurs et destructeurs virtuels sont des frais généraux. Pourquoi? Voyons ça!
Voici des statistiques sur la mémoire à une étape du projet
Taille du programme: 15 458 octets (utilisé 50% d'un maximum de 30 720 octets) (2,45 secondes)
Utilisation minimale de la mémoire: 1258 octets (61% d'un maximum de 2048 octets)
Expérience n ° 1 - réécriture en C.
J'ai organisé des cours, tout réécrit sur des tables avec des pointeurs vers des fonctions. , .
Program size: 14 568 bytes (used 47% of a 30 720 byte maximum) (2,35 secs)
Minimum Memory Usage: 1176 bytes (57% of a 2048 byte maximum)
. 900 80 . . 80 vtable'. ( ) .
, — , . “” . .
, , . . « », . .
№2 — ++
, . . . new/delete.
Program size: 15 408 bytes (used 50% of a 30 720 byte maximum) (2,60 secs)
Minimum Memory Usage: 1273 bytes (62% of a 2048 byte maximum)
. , , . . C'est-à-dire , . .
. , . C'est-à-dire “” . , , .
, . , vtable. :
Program size: 14 704 bytes (used 48% of a 30 720 byte maximum) (2,94 secs)
Minimum Memory Usage: 1211 bytes (59% of a 2048 byte maximum)
vtable' , 2. . ( ), free, (-12 ). (8 ) , (Screen, ParentScreen — 40 )
— 700 . , malloc/free/new/delete. 700 ! 700 , !
-,
: ++ . , . . , , ++?
Postface
. , . . : , , .
— GPS. , .
10 . , ATMega32. , . — ATMega64 STM32.
- . — .
.
.