Multifunktionaler DIY Logger. Teil 1

Bild

Ich bin der Besitzer eines wunderbaren Geräts - GPS-Logger Holux M-241. Das Ding ist sehr praktisch und nützlich auf Reisen. Mit Hilfe eines Loggers schreibe ich einen GPS-Track einer Reise, auf dem Sie sich dann detailliert zurechtfinden und die aufgenommenen Fotos an GPS-Koordinaten anhängen können. Er hat auch einen kleinen Bildschirm, auf dem zusätzliche Informationen angezeigt werden - Stunden, aktuelle Geschwindigkeit, Höhe und Richtung, Kilometerzähler und vieles mehr. Hier habe ich mal eine kurze Rezension geschrieben.

Mit all den Vorteilen eines Stücks Eisen begann ich daraus zu wachsen. Ich vermisse ein paar kleine, aber nützliche Extras: ein paar Kilometerzähler, die die vertikale Geschwindigkeit anzeigen und die Parameter eines Streckenabschnitts messen. Es scheint wie kleine Dinge, aber die Firma Holux fand dies nicht nützlich genug für die Implementierung in die Firmware. Außerdem mag ich einige Parameter der Hardware nicht und einige Dinge sind in 10 Jahren veraltet ...

Irgendwann wurde mir klar, dass ich selbst einen Logger mit den Funktionen erstellen kann, die ich benötige. Glücklicherweise sind alle notwendigen Komponenten recht billig und erschwinglich. Ich begann meine Implementierung basierend auf Arduino zu machen. Unter dem Schnitt ein Bautagebuch, in dem ich versuchte, meine technischen Lösungen zu malen.

Funktionen definieren


Viele werden sich fragen, warum ich meinen eigenen Logger bauen muss, wenn sicher etwas für namhafte Hersteller bereit ist. Möglicherweise. Um ehrlich zu sein, habe ich nicht wirklich danach gesucht. Aber sicher wird etwas fehlen. In jedem Fall ist dieses Projekt ein Fan für mich. Warum bauen wir nicht unser Traumgerät?

Also, für das, was ich an meinem Holux M-241 schätze.

  • Der Bildschirm bildet eine „Black Box“, deren Ergebnisse erst nach der Reise verfügbar sind. Dies ist ein sehr praktisches Tool, dessen Messwerte hier und jetzt verfügbar sind. Ein Bildschirm ermöglicht fast alle Funktionen dieser Liste.
  • Eine Uhr ist an sich nützlich. Auf GPS-Reisen ist der Logger, der an einer Schnur um den Hals baumelt, oft näher als ein Handy in der Tasche oder im Rucksack. Die Uhr unterstützt alle Zeitzonen (allerdings mit manueller Umschaltung)
  • Mit der POI-Schaltfläche können Sie die aktuelle Koordinate auf der Spur markieren. Zum Beispiel, um eine Landmarke zu notieren, die aus dem Fenster des Busses gerutscht ist und über die ich später googeln möchte.
  • Mit dem Kilometerzähler können Sie die zurückgelegte Strecke von einem bestimmten Punkt aus messen. Zum Beispiel die pro Tag zurückgelegte Strecke oder die Länge einer Strecke.
  • Aktuelle Geschwindigkeit, Höhe und Richtung helfen Ihnen, sich im Weltraum zu befinden
  • Die Überlebensfähigkeit von 12 bis 14 Stunden mit einer AA-Batterie ermöglicht es Ihnen in den meisten Fällen, nicht über Probleme bei der Stromversorgung nachzudenken. Das heißt, Fast immer genug Gebühr für einen ganzen Reisetag.
  • Kompakt und einfach zu bedienen - die Dinge in der modernen Welt sind sehr schön

Einige Dinge könnten jedoch etwas besser gemacht werden:

  • Das Stromversorgungssystem für AA-Batterien wird von vielen als eindeutiges Plus bezeichnet - eine Batterie hält lange und Sie können die Versorgung in jeder Wildnis wieder auffüllen. Sie können sich für mindestens einen Monat autonomes Campen eindecken.

    Aber für mich ist die Akkulaufzeit reine Hämorrhoiden. Sie müssen eine Handvoll Batterien mit sich führen und wer weiß, wie hochwertig sie sind (plötzlich lagen sie 5 Jahre lang auf einem Regal und waren bereits selbstentladen). Mit Batterien ist die Blutung noch größer. Mein Ladegerät kann nur paarweise aufgeladen werden. Wir müssen die Batterien entladen, damit sie den gleichen Entladungsgrad haben. Infolgedessen erinnern Sie sich nie daran, wo bereits entladen wurde und wo noch nicht.

    Während 6 Jahren mit dem Logger bin ich nur mitten im Nirgendwo ohne Strom gelandet. In der Regel bekomme ich mindestens einmal am Tag Zugang zur Steckdose. In diesem Fall wäre die eingebaute Lithiumbatterie viel praktischer. In extremen Fällen habe ich eine Paverbank.

  • Die Anzeige des Entladungsgrades erfolgt sehr dumm - die Anzeige beginnt zu blinken, wenn der Akku entladen wird. Darüber hinaus kann es in 5 Minuten sterben und vielleicht eine weitere Stunde arbeiten. Es ist sehr leicht, diesen Moment zu verpassen und einen Teil des Protokolls zu verlieren.

  • Als Luftfahrtinteressierte wäre es für mich sehr interessant, die aktuelle Vertikalgeschwindigkeit zu beobachten.

  • Ein paar Kilometerzähler - es ist oft interessant, mehr als eine Strecke zu messen. Zum Beispiel die pro Tag und für die gesamte Reise zurückgelegte Strecke.

  • Der Kilometerzähler wird zurückgesetzt, wenn Sie das Gerät ausschalten oder den Akku austauschen. Das ist furchtbar unangenehm. Wenn Sie in einem Café eine Mahlzeit eingenommen haben, kann der GPS-Logger nicht ausgeschaltet werden, da der Wert zurückgesetzt wird. Er muss es eingeschaltet lassen und fährt weiter Kilometer und frisst die Batterie. Es wäre viel bequemer, den Kilometerzähler anzuhalten und die Werte zwischen den Einschlüssen zu speichern .

  • Messung von Standortparametern . Beim Skifahren interessiert mich zum Beispiel die Länge der Abfahrt, die Höhe, die Durchschnitts- und Höchstgeschwindigkeit auf der Baustelle und die aufgewendete Zeit. Was Sie wissen möchten, ist sofort und nicht zu Hause, wenn Sie den Titel herunterladen.

  • Die Genauigkeit ist schlecht. Wenn Sie sich schnell bewegen - sonst nichts. Wenn die Geschwindigkeit auf der Strecke jedoch gering ist, sind „Geräusche“ + - 50 m deutlich sichtbar. Und für eine Stunde Stehen können Sie fast einen Kilometer „bestehen“. Der Nutzen der Technologie seit 10 Jahren ist weit fortgeschritten und moderne Empfänger bieten eine viel größere Genauigkeit.

  • Die Geschwindigkeit beim Zusammenführen von Tracks beträgt nur 38.400. Nein, es ist 2017 nicht ernst, den COM-Port zum Übertragen großer Datenmengen zu verwenden. Das Zusammenführen von 2 Megabyte internem Flash dauert mehr als 20 Minuten.

    Darüber hinaus kann nicht jedes Programm das Format zusammengeführter Spuren verarbeiten. Das native Dienstprogramm ist sehr elend. Glücklicherweise gibt es BT747, mit dem der Track angemessen zusammengeführt und in ein verdauliches Format konvertiert werden kann.

  • Die Größe des Flash-Laufwerks beträgt nur 2 MB. Einerseits reicht dies für eine zweiwöchige Reise mit Sparpunkten alle 5 Sekunden. Aber zuerst das interne Paketformat
    erfordert Konvertierung und erlaubt zweitens nicht, die Lautstärke zu erhöhen
  • Massenspeichergerät ist aus irgendeinem Grund jetzt nicht in Mode. Moderne Schnittstellen versuchen, das Vorhandensein von Dateien zu verbergen. Ich bin seit 25 Jahren mit Computern beschäftigt und die direkte Arbeit mit Dateien ist für mich viel bequemer als auf jede andere Weise.

Hier gibt es nichts, was ohne nennenswerten Aufwand nicht realisiert werden könnte.

Alles andere. Ich benutze es nicht selbst, aber plötzlich ist jemand nützlich:

  • Zeigt die aktuellen Koordinaten (Breite, Länge) an.
  • Auf der linken Seite des Bildschirms sind verschiedene Symbole gezeichnet, an deren Essenz ich mich ohne Handbuch nicht erinnern kann.
  • Es gibt Schaltmeter / km - Fuß / Meilen.
  • Der Bluetooth-Logger kann ohne GPS mit Mobiltelefonen verbunden werden.
  • Der absolute Abstand zum Punkt.
  • Protokollierung nach Zeit (alle N Sekunden) oder nach Entfernung (alle X Meter).
  • Unterstützung für verschiedene Sprachen.

Wähle Eisen


Die Anforderungen sind mehr oder weniger definiert. Es ist Zeit zu verstehen, wie all dies umgesetzt werden kann. Die Hauptkomponenten, die ich haben werde, sind:

  • Mikrocontroller - Ich habe keine Pläne für ausgefeilte Rechenalgorithmen, daher ist die Verarbeitungsleistung des Kernels nicht besonders wichtig. Ich habe auch keine besonderen Anforderungen an die Befüllung - ein Satz Standardperipheriegeräte reicht aus.

    Zur Hand war nur eine Streuung verschiedener Arduinoes sowie ein paar stm32f103c8t6. Ich habe mich für AVR entschieden, das ich auf der Ebene der Controller / Register / Peripheriegeräte gut kenne. Wenn ich auf Einschränkungen stoße, gibt es einen Grund, den STM32 zu spüren.

  • Der GPS-Empfänger wurde aus den Modulen NEO6MV2, Beitan BN-800 und Beitan BN-880 ausgewählt. Seit einiger Zeit gegoogelte Foren. Erfahrene Leute sagten, dass der erste Empfänger das letzte Jahrhundert ist. Die beiden anderen unterscheiden sich nur in der Position der Antenne - beim BN-800 hängt sie am Draht und beim BN-880 wird sie mit einem Sandwich auf das Hauptmodul geklebt. Nahm einen BN-880 .

  • Bildschirm - Das Original verwendet ein 128 x 32 LCD mit Hintergrundbeleuchtung. Ich habe nicht genau das gleiche gefunden. Ich habe eine OLED 0,91 Zoll auf dem SSD1306-Controller und einen 1,2-Zoll-LCD-Bildschirm auf dem ST7565R-Controller gekauft . Ich habe mich entschlossen, von vorne zu beginnen, weil Es ist einfacher, eine Verbindung mit einem Standardkamm gemäß I2C oder SPI herzustellen. Es ist jedoch im Vergleich zum Original etwas kleiner und funktioniert auch aus Gründen der Kraftstoffeffizienz nicht, um das Bild ständig anzuzeigen. Das zweite Display sollte weniger gefräßig sein, aber Sie müssen einen kniffligen Stecker dafür löten und herausfinden, wie die Hintergrundbeleuchtung mit Strom versorgt wird.

Von den kleinen Dingen:

  • Knöpfe kauften einmal eine ganze Tasche;
  • Schild mit für SD-Karte - auch zur Hand herumliegen;
  • Ich habe ein Paar verschiedene Laderegler für Lithiumbatterien gekauft, aber ich habe es immer noch nicht verstanden.

Ich habe beschlossen, das Board ganz am Ende zu entwerfen, wenn die Firmware fertig ist. Zu diesem Zeitpunkt werde ich endlich über die Hauptkomponenten und das Schema für ihre Aufnahme entscheiden. In der ersten Phase entschied ich mich für das Debuggen auf dem Steckbrett, indem ich die Komponenten mit Patchkabeln verband.

Aber zuerst müssen Sie sich für ein sehr wichtiges Thema entscheiden - die Ernährung der Komponenten. Es schien mir vernünftig, alles von 3,3 V: GPS und dem Bildschirm nur darauf zu betreiben und zu wissen, wie man arbeitet. Dies ist auch die native Spannung für USB und SD. Zusätzlich kann die Schaltung mit einer Lithiumdose betrieben werden.

Die Wahl fiel auf den Arduino Pro Mini, der in der 8MHz / 3.3V-Version zu finden ist. Aber sie hatte kein USB an Bord - ich musste einen USB-UART-Adapter verwenden.

Erste Schritte


Ursprünglich wurde das Projekt in Arduino IDE erstellt. Aber um ehrlich zu sein, meine Sprache traut sich nicht, es als IDE zu bezeichnen - wie ein Texteditor mit einem Compiler. Auf jeden Fall kann ich nach Visual Studio, in dem ich seit 13 Jahren arbeite, in der Arduino IDE nichts Ernstes ohne Tränen und Matyuk tun.

Zum Glück gibt es ein kostenloses Atmel Studio, in das sogar Visual Assist sofort integriert ist !!! Das Programm weiß alles, was benötigt wird, alles ist vertraut und an seinem Platz. Nun, fast alles (ich habe nicht gefunden, wie man nur eine Datei kompiliert, um beispielsweise die Syntax zu überprüfen)

Bild

Vom Bildschirm aus gestartet - Dies ist erforderlich, um das Skelett der Firmware zu debuggen und es dann mit Funktionen zu füllen. Er blieb bei der ersten verfügbaren Bibliothek für Adafruit SSD1306 stehen . Sie weiß alles, was benötigt wird und bietet eine sehr einfache Oberfläche.

Spielte mit Schriftarten. Es stellte sich heraus, dass eine Schriftart bis zu 8 KB groß sein kann (die Größe der Buchstaben beträgt 24pt) - in einem 32-KB-Controller kann man nicht besonders herumlaufen. Zum Anzeigen der Zeit werden beispielsweise große Schriftarten benötigt.

Beispielcode für Schriftarten
#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; } 


Schriftarten mit der Bibliothek sind sehr ungeschickt. Die Monospace-Schrift stellte sich als sehr breit heraus - die Zeile „12:34:56“ passt nicht, Serif - alle Zahlen haben unterschiedliche Gewichte. Es sei denn, die Standardschriftart 5x7 in der Bibliothek sieht essbar aus.

Bild

Bild

Es stellte sich heraus, dass diese Schriftarten von einigen Open-Source-ttf-Schriftarten konvertiert wurden, die einfach nicht für kleine Auflösungen optimiert sind.

Ich musste meine Schriften zeichnen. Genauer gesagt, graben Sie zuerst einzelne Symbole aus den fertigen aus. Das Symbol ':' in der ASCII-Tabelle ist direkt nach den Zahlen sehr nützlich und kann in einem Block gekauft werden. Es ist auch praktisch, dass Sie eine Schriftart nicht für alle Zeichen erstellen können, sondern nur für einen Bereich, z. B. von 0x30 ('0') bis 0x3a (':'). T.O. Bei FreeSans18pt7b stellte sich heraus, dass nur für die erforderlichen Zeichen eine sehr kompakte Schriftart erstellt wurde. Richtig, ich musste die Breite leicht anpassen, damit der Text in die Breite des Bildschirms passte.

Unterschrift FreeSans18pt7b
 // This font consists only of digits and ':' to display current time. // The font is very based on FreeSans18pt7b.h //TODO: 25 pixel height is too much for displaying time. Create another 22px font const uint8_t TimeFontBitmaps[] PROGMEM = { /* 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xE9, 0x20, 0x3F, 0xFC, 0xE3, 0xF1, 0xF8, 0xFC, 0x7E, 0x3F, 0x1F, 0x8E, 0x82, 0x41, 0x00, 0x01, 0xC3, 0x80, ... 0x03, 0x00, 0xC0, 0x60, 0x18, 0x06, 0x03, 0x00, 0xC0, 0x30, 0x18, 0x06, 0x01, 0x80, 0xC0, 0x30, 0x00, */0x07, 0xE0, 0x0F, 0xF8, 0x1F, 0xFC, 0x3C, 0x3C, 0x78, 0x1E, 0x70, 0x0E, 0x70, 0x0E, 0xE0, 0x07, 0xE0, 0x07, 0xE0, 0x07, 0xE0, 0x07, 0xE0, 0x07, 0xE0, 0x07, 0xE0, 0x07, 0xE0, 0x07, 0xE0, 0x07, 0xE0, 0x07, 0xE0, 0x0F, 0x70, 0x0E, 0x70, 0x0E, 0x78, 0x1E, 0x3C, 0x3C, 0x1F, 0xF8, 0x1F, 0xF0, 0x07, 0xE0, 0x03, 0x03, 0x07, 0x0F, 0x3F, 0xFF, 0xFF, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0xE0, 0x1F, 0xF8, 0x3F, 0xFC, 0x7C, 0x3E, 0x70, 0x0F, 0xF0, 0x0F, 0xE0, 0x07, 0xE0, 0x07, 0x00, 0x07, 0x00, 0x07, 0x00, 0x0F, 0x00, 0x1E, 0x00, 0x3C, 0x00, 0xF8, 0x03, 0xF0, 0x07, 0xC0, 0x1F, 0x00, 0x3C, 0x00, 0x38, 0x00, 0x70, 0x00, 0x60, 0x00, 0xE0, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x07, 0xF0, 0x07, 0xFE, 0x07, 0xFF, 0x87, 0x83, 0xC3, 0x80, 0xF3, 0x80, 0x39, 0xC0, 0x1C, 0xE0, 0x0E, 0x00, 0x07, 0x00, 0x0F, 0x00, 0x7F, 0x00, 0x3F, 0x00, 0x1F, 0xE0, 0x00, 0x78, 0x00, 0x1E, 0x00, 0x07, 0x00, 0x03, 0xF0, 0x01, 0xF8, 0x00, 0xFE, 0x00, 0x77, 0x00, 0x73, 0xE0, 0xF8, 0xFF, 0xF8, 0x3F, 0xF8, 0x07, 0xF0, 0x00, 0x00, 0x38, 0x00, 0x38, 0x00, 0x78, 0x00, 0xF8, 0x00, 0xF8, 0x01, 0xF8, 0x03, 0xB8, 0x03, 0x38, 0x07, 0x38, 0x0E, 0x38, 0x1C, 0x38, 0x18, 0x38, 0x38, 0x38, 0x70, 0x38, 0x60, 0x38, 0xE0, 0x38, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x38, 0x00, 0x38, 0x00, 0x38, 0x00, 0x38, 0x00, 0x38, 0x00, 0x38, 0x1F, 0xFF, 0x0F, 0xFF, 0x8F, 0xFF, 0xC7, 0x00, 0x03, 0x80, 0x01, 0xC0, 0x00, 0xE0, 0x00, 0x70, 0x00, 0x39, 0xF0, 0x3F, 0xFE, 0x1F, 0xFF, 0x8F, 0x83, 0xE7, 0x00, 0xF0, 0x00, 0x3C, 0x00, 0x0E, 0x00, 0x07, 0x00, 0x03, 0x80, 0x01, 0xC0, 0x00, 0xFC, 0x00, 0xEF, 0x00, 0x73, 0xC0, 0xF0, 0xFF, 0xF8, 0x3F, 0xF8, 0x07, 0xE0, 0x00, 0x03, 0xE0, 0x0F, 0xF8, 0x1F, 0xFC, 0x3C, 0x1E, 0x38, 0x0E, 0x70, 0x0E, 0x70, 0x00, 0x60, 0x00, 0xE0, 0x00, 0xE3, 0xE0, 0xEF, 0xF8, 0xFF, 0xFC, 0xFC, 0x3E, 0xF0, 0x0E, 0xF0, 0x0F, 0xE0, 0x07, 0xE0, 0x07, 0xE0, 0x07, 0x60, 0x07, 0x70, 0x0F, 0x70, 0x0E, 0x3C, 0x3E, 0x3F, 0xFC, 0x1F, 0xF8, 0x07, 0xE0, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x06, 0x00, 0x0E, 0x00, 0x1C, 0x00, 0x18, 0x00, 0x38, 0x00, 0x70, 0x00, 0x60, 0x00, 0xE0, 0x00, 0xC0, 0x01, 0xC0, 0x01, 0x80, 0x03, 0x80, 0x03, 0x80, 0x07, 0x00, 0x07, 0x00, 0x07, 0x00, 0x0E, 0x00, 0x0E, 0x00, 0x0E, 0x00, 0x0C, 0x00, 0x1C, 0x00, 0x1C, 0x00, 0x07, 0xF0, 0x0F, 0xFE, 0x0F, 0xFF, 0x87, 0x83, 0xC7, 0x80, 0xF3, 0x80, 0x39, 0xC0, 0x1C, 0xE0, 0x0E, 0x78, 0x0F, 0x1E, 0x0F, 0x07, 0xFF, 0x01, 0xFF, 0x03, 0xFF, 0xE3, 0xE0, 0xF9, 0xC0, 0x1D, 0xC0, 0x0F, 0xE0, 0x03, 0xF0, 0x01, 0xF8, 0x00, 0xFC, 0x00, 0xF7, 0x00, 0x73, 0xE0, 0xF8, 0xFF, 0xF8, 0x3F, 0xF8, 0x07, 0xF0, 0x00, 0x07, 0xE0, 0x1F, 0xF8, 0x3F, 0xFC, 0x7C, 0x3C, 0x70, 0x0E, 0xF0, 0x0E, 0xE0, 0x06, 0xE0, 0x07, 0xE0, 0x07, 0xE0, 0x07, 0xE0, 0x0F, 0x70, 0x0F, 0x78, 0x3F, 0x3F, 0xFF, 0x1F, 0xF7, 0x07, 0xC7, 0x00, 0x07, 0x00, 0x06, 0x00, 0x0E, 0x70, 0x0E, 0x70, 0x1C, 0x78, 0x3C, 0x3F, 0xF8, 0x1F, 0xF0, 0x07, 0xC0, 0xFF, 0xF0, 0x00, 0x00, 0x00, 0x07, 0xFF, 0x80 /*, 0xFF, 0xF0, 0x00, 0x00, 0x00, 0x07, 0xFF, 0xB6, 0xD6, 0x00, 0x00, 0x80, 0x03, 0xC0, 0x07, 0xE0, 0x0F, 0xC0, 0x3F, 0x80, 0x7E, 0x00, 0xFC, 0x01, 0xF0, 0x00, 0xE0, 0x00, ... 0x38, 0x38, 0xF8, 0xF0, 0xE0, 0x38, 0x00, 0xFC, 0x03, 0xFC, 0x1F, 0x3E, 0x3C, 0x1F, 0xE0, 0x1F, 0x80, 0x1E, 0x00 */ }; //TODO Recalc offset numbers const GFXglyph TimeFontGlyphs[] PROGMEM = { { 449-449, 16, 25, 19, 2, -24 }, // 0x30 '0' { 499-449, 8, 25, 19, 4, -24 }, // 0x31 '1' { 524-449, 16, 25, 19, 2, -24 }, // 0x32 '2' { 574-449, 17, 25, 19, 1, -24 }, // 0x33 '3' { 628-449, 16, 25, 19, 1, -24 }, // 0x34 '4' { 678-449, 17, 25, 19, 1, -24 }, // 0x35 '5' { 732-449, 16, 25, 19, 2, -24 }, // 0x36 '6' { 782-449, 16, 25, 19, 2, -24 }, // 0x37 '7' { 832-449, 17, 25, 19, 1, -24 }, // 0x38 '8' { 886-449, 16, 25, 19, 1, -24 }, // 0x39 '9' { 936-449, 3, 19, 7, 2, -20 }, // 0x3A ':' }; const GFXfont TimeFont PROGMEM = { (uint8_t *)TimeFontBitmaps, (GFXglyph *)TimeFontGlyphs, 0x30, 0x3A, 20 }; 

Es stellte sich heraus, dass die 18pt-Schrift tatsächlich 25 Pixel hoch ist. Aus diesem Grund passt er leicht auf eine andere Inschrift

Bild

Die invertierte Anzeige hilft übrigens zu verstehen, wo sich die Grenzen des Zeichenbereichs tatsächlich befinden und wie die Linie relativ zu diesem Rand liegt - die Anzeige hat sehr große Rahmen.

Googelte lange Zeit fertige Schriftarten, aber sie passten weder in Größe, Form noch Inhalt. Zum Beispiel im Internet eine 8x12-Schriftwelle (Speicherauszüge von VGA-Kartenzeichengeneratoren). Tatsächlich sind diese Schriftarten jedoch 6 x 8, d. H. Viele Weltraumspaziergänge - bei einer so geringen Auflösung und Größe wie meiner ist dies von entscheidender Bedeutung.

Ich musste meine eigenen Schriftarten zeichnen, da das Schriftformat der Adafruit-Bibliothek sehr einfach ist. Ich habe das Bild in Paint.net vorbereitet - ich habe einfach die Buchstaben in der richtigen Schrift gezeichnet und sie dann mit einem Bleistift ein wenig korrigiert. Ich habe das Bild als PNG gespeichert und es dann schnell an das Python-Skript gesendet, das auf meinem Knie geschrieben ist. Dieses Skript hat einen halbfertigen Code generiert, der bereits punktweise in der IDE direkt in den Hex-Codes regiert.

Bild

So sieht beispielsweise der Prozess zum Erstellen einer monospaced Schriftart 8x12 mit kleinem Buchstaben- und Zeilenabstand aus. Jedes Zeichen am Ende war ungefähr 7x10 groß und belegte standardmäßig 10 Bytes. Es wäre möglich, jedes Zeichen in 8-9 Bytes zu packen (die Bibliothek erlaubt dies), aber ich habe mich nicht darum gekümmert. Darüber hinaus können Sie in diesem Formular einzelne Pixel direkt im Code bearbeiten.

Schriftart 8x12
 // A simple 8x12 font (slightly modifier Courier New) const uint8_t Monospace8x12Bitmaps[] PROGMEM = { 0x1e, 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, 0x1e, //0 0x18, 0x68, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x7f, //1 0x3e, 0x41, 0x41, 0x01, 0x02, 0x0c, 0x10, 0x20, 0x41, 0x7f, //2 0x3e, 0x41, 0x01, 0x01, 0x0e, 0x02, 0x01, 0x01, 0x41, 0x3e, //3 0x02, 0x06, 0x0a, 0x12, 0x12, 0x22, 0x3f, 0x02, 0x02, 0x0f, //4 0x7f, 0x41, 0x40, 0x40, 0x7e, 0x01, 0x01, 0x01, 0x41, 0x3e, //5 0x1e, 0x21, 0x40, 0x40, 0x5e, 0x61, 0x41, 0x41, 0x41, 0x3e, //6 0x7f, 0x41, 0x01, 0x02, 0x02, 0x04, 0x04, 0x04, 0x08, 0x08, //7 0x1e, 0x21, 0x21, 0x21, 0x1e, 0x21, 0x21, 0x21, 0x21, 0x1e, //8 0x1e, 0x21, 0x21, 0x21, 0x23, 0x1d, 0x01, 0x01, 0x22, 0x1c, //9 0x00, 0x00, 0x18, 0x18, 0x00, 0x00, 0x00, 0x18, 0x18, 0x00, //: }; const GFXglyph Monospace8x12Glyphs[] PROGMEM = { { 0, 8, 10, 8, 0, -11 }, // 0x30 '0' { 10, 8, 10, 8, 0, -11 }, // 0x31 '1' { 20, 8, 10, 8, 0, -11 }, // 0x32 '2' { 30, 8, 10, 8, 0, -11 }, // 0x33 '3' { 40, 8, 10, 8, 0, -11 }, // 0x34 '4' { 50, 8, 10, 8, 0, -11 }, // 0x35 '5' { 60, 8, 10, 8, 0, -11 }, // 0x36 '6' { 70, 8, 10, 8, 0, -11 }, // 0x37 '7' { 80, 8, 10, 8, 0, -11 }, // 0x38 '8' { 90, 8, 10, 8, 0, -11 }, // 0x39 '9' { 100, 8, 10, 8, 0, -11 }, // 0x3A ':' }; const GFXfont Monospace8x12Font PROGMEM = { (uint8_t *)Monospace8x12Bitmaps, (GFXglyph *)Monospace8x12Glyphs, 0x30, 0x3A, 12 }; 


Rahmen


Das Originalgerät bietet eine sehr einfache und bequeme Oberfläche. Informationen werden in Kategorien gruppiert, die auf einzelnen Seiten (Bildschirmen) angezeigt werden. Mit der Schaltfläche können Sie durch die Seiten blättern und mit der zweiten Schaltfläche das aktuelle Element auswählen oder die in der Signatur unter der Schaltfläche angegebene Aktion ausführen. Dieser Ansatz erscheint mir sehr praktisch und es besteht keine Notwendigkeit, etwas zu ändern.

Ich mag die Schönheit von OOP, weil ich sofort eine kleine Benutzeroberfläche geblendet habe. Jede Seite implementiert die Benutzeroberfläche nach Bedarf. Die Seite kann sich selbst zeichnen und implementiert die Reaktion auf die Schaltflächen.

 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); }; 

Schaltflächen können je nach aktuellem Bildschirm verschiedene Aktionen ausführen. Daher habe ich den oberen Rand des Bildschirms mit einer Höhe von 8 Pixel den Beschriftungen für die Schaltflächen zugewiesen. Der Text für die Signaturen hängt vom aktuellen Bildschirm ab und wird von den virtuellen Funktionen getSelButtonText () und getOkButtonText () zurückgegeben. Auch im Header werden weiterhin Serviceelemente wie GPS-Signalstärke und Batterieladung angezeigt. Die verbleibenden ¾ Bildschirme stehen für nützliche Informationen zur Verfügung.

Wie gesagt, die Bildschirme können umgedreht werden, was bedeutet, dass irgendwo eine Liste von Objekten für verschiedene Seiten vorhanden sein sollte. An welchen mehr als einem Bildschirm können Sie wie in einem Untermenü verschachteln. Ich habe sogar die ScreenManager-Klasse gestartet, die diese Listen verwalten sollte, aber dann fand ich die Lösung einfacher.

Jeder Bildschirm hat also einfach einen Zeiger auf den nächsten. Wenn Sie auf dem Bildschirm das Untermenü aufrufen können, wird dem Bildschirm dieses Untermenüs ein weiterer Zeiger hinzugefügt

 class Screen { Screen * nextScreen; … }; class ParentScreen : public Screen { Screen * childScreen; … }; 

Standardmäßig ruft der Schaltflächenhandler einfach die Bildschirmänderungsfunktion auf und übergibt ihr den gewünschten Zeiger. Die Funktion erwies sich als trivial - sie schaltete nur den Zeiger auf den aktuellen Bildschirm. Um die Verschachtelung der Bildschirme sicherzustellen, habe ich einen kleinen Stapel erstellt. Der gesamte Bildschirmmanager passt also in 25 Zeilen und 4 kleine Funktionen.

 Screen * screenStack[3]; int screenIdx = 0; void setCurrentScreen(Screen * screen) { screenStack[screenIdx] = screen; } Screen * getCurrentScreen() { return screenStack[screenIdx]; } void enterChildScreen(Screen * screen) { screenIdx++; //TODO limit this screenStack[screenIdx] = screen; } void backToParentScreen() { if(screenIdx) screenIdx--; } 

Der Code zum Füllen dieser Strukturen sieht zwar nicht besonders gut aus, wurde aber bisher nicht besser erfunden.

 Screen * createCurrentTimeScreen() { TimeZoneScreen * tzScreen = new TimeZoneScreen(1, 30); tzScreen = tzScreen->addScreen(new TimeZoneScreen(2, 45)); tzScreen = tzScreen->addScreen(new TimeZoneScreen(-3, 30)); // TODO Add real timezones here CurrentTimeScreen * screen = new CurrentTimeScreen(); screen->addChildScreen(tzScreen); return screen; } 

Der Gedanke
Die Strukturierung hat sich natürlich als schön herausgestellt, aber ich fürchte, sie frisst viel Gedächtnis. Sie müssen gegen sich selbst gehen und eine große statische Tabelle mit Zeigern zafigachit.

Mach weiter. Bei der Implementierung der Schnittstelle wollte ich so etwas wie ein Meldungsfeld erstellen, eine kurze Nachricht, die ein oder zwei Sekunden lang angezeigt und dann ausgeblendet wird. Wenn Sie beispielsweise die POI-Taste (Point of Interest) auf dem Bildschirm mit den aktuellen Koordinaten drücken, ist es hilfreich, dem Benutzer zusätzlich zum Schreiben des Punkts auf die Spur die Meldung „Wegpunkt gespeichert“ anzuzeigen (im Originalgerät wird nur für eine Sekunde ein zusätzliches Symbol angezeigt). Wenn der Akku fast leer ist, können Sie den Benutzer mit einer Meldung „aufmuntern“.

Bild

Da die Daten von GPS ständig eingehen, kann von keinen Sperrfunktionen gesprochen werden. Daher musste ich eine einfache Zustandsmaschine (Zustandsmaschine) erfinden, die in der Funktion loop () auswählt, was zu tun ist - den aktuellen Bildschirm oder das aktuelle Meldungsfeld anzeigen.

 enum State { IDLE_DISPLAY_OFF, IDLE, MESSAGE_BOX, BUTTON_PRESSED, }; 

Es ist auch bequem, Tastendrücke mit der Zustandsmaschine zu handhaben. Vielleicht wäre es durch Interrupts richtig, aber es hat sich auch als gut herausgestellt. Das funktioniert so: Wenn eine Taste im IDLE-Status gedrückt wurde, merken Sie sich die Zeit, zu der sie gedrückt wurde, und wechseln Sie in den Status BUTTON_PRESSED. In diesem Zustand warten wir, bis der Benutzer die Schaltfläche loslässt. Hier können wir die Dauer berechnen, zu der die Taste gedrückt wurde. Kurze Antworten (<30 ms) werden einfach ignoriert - höchstwahrscheinlich handelt es sich um einen Kontaktsprung. Lange Fahrten können bereits als Tastendruck interpretiert werden.

Ich plane, sowohl kurzes Drücken von Tasten für normale Aktionen als auch langes (> 1c) für spezielle Funktionen zu verwenden. Zum Beispiel startet / pausiert ein kurzes Drücken den Kilometerzähler, ein langes Drücken setzt den Zähler auf 0 zurück.

Vielleicht werden andere Staaten hinzugefügt. So ändern sich beispielsweise im ursprünglichen Logger nach dem Umschalten auf die nächste Seite die Werte auf dem Bildschirm häufig und nach einigen Sekunden seltener - einmal pro Sekunde. Dies kann durch Hinzufügen eines weiteren Status erfolgen.

Als der Rahmen fertig war, begann ich bereits, GPS anzuschließen. Aber hier gab es Nuancen, die mich veranlassten, diese Aufgabe zu verschieben.

Firmware-Optimierung


Bevor ich weitermache, muss ich mich von einigen technischen Details ablenken lassen. Tatsache ist, dass ich ungefähr an diesem Ort anfing, mit zunehmendem Speicherverbrauch zu stoßen. Es stellte sich heraus, dass die Zeile, die zu Beginn der Firmware ohne den Modifikator PROGMEM rücksichtslos deklariert wurde, in den Arbeitsspeicher kopiert wird und dort während der gesamten Laufzeit Speicherplatz beansprucht.

Verschiedene Architekturen
Kurzgesagt. Auf großen Computern wird die Von Neumann-Architektur verwendet, bei der sich Code und Daten im selben Adressraum befinden. Das heißt, Daten aus RAM und ROM werden auf die gleiche Weise gelesen.

Mikrocontroller verwenden normalerweise die Harvard-Architektur , bei der Code und Daten getrennt sind. T.O. Sie müssen verschiedene Funktionen verwenden, um Speicher und Flash zu lesen. Aus Sicht der C / C ++ - Sprache sehen Zeiger gleich aus, aber beim Schreiben eines Programms müssen wir genau wissen, auf welchen Speicher unser Zeiger genau zeigt, und die entsprechenden Funktionen aufrufen.

Glücklicherweise haben sich Bibliotheksentwickler bereits teilweise darum gekümmert. Die Hauptklasse der Anzeigebibliothek - Adafruit_SSD1306 - wird von der Print-Klasse der Arduino-Standardbibliothek geerbt.

Dies bietet uns eine ganze Reihe verschiedener Modifikationen der Druckmethode - zum Drucken von Zeichenfolgen, einzelnen Zeichen, Zahlen und etwas anderem. Es hat also 2 separate Funktionen zum Drucken von Zeilen:

 size_t print(const __FlashStringHelper *); size_t print(const char[]); 

Der erste weiß, dass Sie eine Zeile von einem Flash-Laufwerk drucken müssen, und lädt sie Zeichen für Zeichen. Der zweite druckt Zeichen aus dem RAM. Tatsächlich nehmen beide Funktionen einen Zeiger auf eine Zeichenfolge nur aus unterschiedlichen Adressräumen.

Lange habe ich im Arduino-Code nach diesem __FlashStringHelper gesucht, um zu lernen, wie man die gewünschte print () -Funktion aufruft. Es stellte sich heraus, dass die Jungs den Trick gemacht hatten: Sie deklarierten diesen Typ einfach mit der Vorwärtsdeklaration (ohne den Typ selbst zu deklarieren) und schrieben ein Makro, das blitzschnell Zeiger auf Zeilen in den Typ __FlashStringHelper umwandelte. Nur damit der Compiler die erforderliche überladene Funktion auswählt

 class __FlashStringHelper; #define F(string_literal) (reinterpret_cast<const __FlashStringHelper *>(PSTR(string_literal))) 

So können Sie wie folgt schreiben:

 display.print(F(“String in flash memory”)); 


Aber du kannst nicht so schreiben
 const char text[] PROGMEM = "String in flash memory"; display.print(F(text)); 

Und anscheinend bietet die Bibliothek nichts, was auf diese Weise getan werden könnte. Ich weiß, dass es nicht gut ist, private Bibliotheksstücke in meinem Code zu verwenden, aber was soll ich tun? Ich zeichnete mein Makro, das tat, was ich brauchte.

 #define USE_PGM_STRING(x) reinterpret_cast<const __FlashStringHelper *>(x) 

Die Hutzeichnungsfunktion sah also folgendermaßen aus:

 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())); } 

Nun, da ich mich mit den Low-Level-Teilen der Firmware befasst habe, habe ich mich entschlossen, genauer zu untersuchen, wie das alles im Inneren funktioniert.

Im Allgemeinen müssen die Leute, die sich Arduino ausgedacht haben, ein Denkmal errichten. Sie bildeten eine einfache und bequeme Plattform für Prototyping und Handwerk. Eine große Anzahl von Menschen mit minimalen Kenntnissen in Elektronik und Programmierung konnte in die Welt von Arduino eintreten. Aber all dies ist glatt und schön, wenn Sie Müll wie Blinker mit LEDs oder das Ablesen des Thermometers machen. Sobald Sie etwas Ernstes anschwingen, müssen Sie sofort tiefer verstehen, als Sie von Anfang an wollten.

Nach jeder hinzugefügten Bibliothek oder Klasse habe ich festgestellt, wie schnell der Speicherverbrauch wächst. Zu diesem Zeitpunkt war ich mit mehr als 14 KB 32 KB Flash und 1300 Byte RAM (von 2 KB) beschäftigt. Jede unachtsame Bewegung erhöhte die bereits verwendete um weitere 10 Prozent. Aber ich habe GPS- und SD / FAT32-Bibliotheken immer noch nicht wirklich verbunden, und die Katze selbst hat geweint. Ich musste den Disassembler- Checker in die Hand nehmen und untersuchen, was der Compiler tat.

Ich habe insgeheim gehofft, dass der Linker nicht verwendete Funktionen auslöst. Es stellte sich jedoch heraus, dass einige von ihnen der Linker fast vollständig einfügt. In der Firmware habe ich die Strichzeichnungsfunktionen und einige andere aus der Bibliothek für die Arbeit mit dem Bildschirm gefunden, obwohl ich sie im Code zu diesem Zeitpunkt offensichtlich nicht aufgerufen habe. Implizit sollten sie auch nicht aufgerufen werden - warum brauche ich eine Strichzeichnungsfunktion, wenn ich nur Buchstaben aus Bitmaps zeichne? Mehr als 5,2 KB aus heiterem Himmel (und das zählt keine Schriftarten).

Neben der Anzeigesteuerungsbibliothek fand ich auch:

  • 2,6 kb - auf SoftwareSerial (ich habe es irgendwann in das Projekt gezogen)
  • 1,6 kb - I2C
  • 1,3 kb - HardwareSerial
  • 2 kb - TinyGPS
  • 2,5 kb auf dem eigentlichen Arduino (Initialisierung, Pins, alle Arten von Tabellen, der Haupttimer für die Funktionen millis () und delay ()),

Die Zahlen sind sehr bezeichnend, wie Der Optimierer mischt ernsthaft Code. Einige Funktionen können an einer Stelle beginnen, und dann kann eine andere aus einer anderen Bibliothek, die von der ersten aufgerufen wird, unmittelbar darauf folgen. Darüber hinaus können sich am anderen Ende des Blitzes separate Zweige dieser Funktionen befinden.

Auch im Code habe ich gefunden:

  • Bildschirmsteuerung durch SPI (obwohl ich es über I2C verbunden habe)
  • Methoden von Basisklassen, die selbst nicht aufgerufen werden, weil in den Erben neu definiert
  • Destruktoren, die niemals vom Design her aufgerufen werden
  • Zeichenfunktionen (und nicht alle - Teil der Funktionen, die der Linker noch ausgeführt hat)
  • malloc / free während in meinem Code alle Objekte im Wesentlichen statisch sind

Aber nicht nur der Verbrauch von Flash-Speicher, sondern auch SRAM wächst sprunghaft:

  • 130 Bytes - I2C
  • 100 Bytes - SoftwareSerial
  • 157 Bytes - Seriell
  • 558 Bytes - Anzeige (von denen 512 der Bildspeicher ist)

Nicht weniger unterhaltsam war der Abschnitt .data. Es gibt ungefähr 700 Bytes und dieses Ding wird zu Beginn von einem Flash in den RAM geladen. Es stellte sich heraus, dass im Speicher und zusammen mit den Initialisierungswerten Plätze für Variablen reserviert sind. Hier leben die Variablen und Konstanten, die Sie vergessen haben, als const PROGMEM zu deklarieren.

Darunter befand sich ein kräftiges Array mit einem „Begrüßungsbildschirm“ des Bildschirms - den Anfangswerten des Bildpuffers. Theoretisch können Sie, wenn Sie den Bildschirm display () unmittelbar nach dem Start erstellen, die Blume und die Adafruit-Inschrift sehen, aber in meinem Fall ist es sinnlos, Flash-Speicher dafür zu verwenden.

Der Abschnitt .data enthält auch vtables. Sie werden von einem Flash-Laufwerk in den Speicher kopiert, anscheinend aus Gründen der Laufzeiteffizienz. Aber Sie müssen ein ziemlich großes Stück RAM opfern - über ein Dutzend Klassen mit mehr als 150 Bytes. Darüber hinaus scheint es keinen Compiler-Schlüssel zu geben, der unter Leistungseinbußen virtuelle Tabellen im Flash-Speicher belässt.

Was tun? Ich weiß es noch nicht. Es wird davon abhängen, wie der Verbrauch weiter wachsen wird. Für gut gefundene Pfosten müssen gnadenlos repariert werden. Höchstwahrscheinlich muss ich alle Bibliotheken explizit in mein Projekt einbeziehen und sie dann gründlich behandeln. Möglicherweise müssen Sie einige der Teile anders umschreiben, um das Gedächtnis zu optimieren. Oder wechseln Sie zu leistungsfähigerer Hardware. Auf jeden Fall weiß ich jetzt über das Problem Bescheid und es gibt eine Strategie, wie man es behebt.

UPDATE:
Geringe Fortschritte bei der Ressourceneffizienz. Ich mache ein Update für diesen Teil, weil im nächsten möchte ich mich auf ganz andere dinge konzentrieren.

In Kommentaren gibt es einige Verwirrung über die Verwendung von C ++. Insbesondere, warum ist er so schlecht und hält vtable in wertvollem RAM? Im Allgemeinen sind virtuelle Funktionen, Konstruktoren und Destruktoren Overhead. Warum? Lass es uns herausfinden!

Hier finden Sie Statistiken zum Speicher zu einem bestimmten Zeitpunkt des Projekts
Programmgröße: 15 458 Byte (verwendet 50% eines Maximums von 30 720 Byte) (2,45 Sekunden)
Minimale Speichernutzung: 1258 Byte (61% eines Maximums von 2048 Byte)

Experiment Nr. 1 - Umschreiben auf C.

Ich warf Klassen, schrieb alles auf Tabellen mit Zeigern auf Funktionen um.Da Bildschirme tatsächlich immer dieselbe Struktur haben, sind alle Datenelemente zu normalen globalen Variablen geworden.

Statistik nach dem Refactoring
Programmgröße: 14 568 Byte (verwendet 47% eines Maximums von 30 720 Byte) (2,35 Sekunden)
Minimale Speichernutzung: 1176 Byte (57% eines Maximums von 2048 Byte)

Gesamt. Gewann 900 Bytes Flash und 80 Bytes RAM. Was genau den Blitz übrig ließ, grub nicht. 80 Byte RAM sind nur so groß wie die von vtable. Alle anderen Daten (Klassenmitglieder) blieben irgendwie erhalten.

Ich muss sagen, dass ich nicht alles verdorben habe - ich wollte nur das große Ganze sehen, ohne viel Zeit damit zu verbringen. Nach dem Refactoring habe ich verschachtelte Screenshots "verloren". Mit ihnen wäre der Verbrauch etwas höher.

Das Wichtigste bei diesem Experiment ist jedoch, dass sich die Qualität des Codes erheblich verschlechtert hat. Der Code einer Funktion ist auf mehrere Dateien verteilt. Für einige Daten gab es "einen Eigentümer" nicht mehr, einige Module begannen, in die Erinnerung anderer zu klettern. Der Code ist pauschal und hässlich geworden.

Experiment Nr. 2 - Bytes aus C ++ quetschen

Ich habe mein Experiment zurückgesetzt und beschlossen, alles als Klassen zu belassen. Nur dieses Mal werden die Bildschirme für statisch verteilte Objekte ausgeführt. Die Struktur der Seiten auf meinen Bildschirmen ist festgelegt. Sie können es bei der Kompilierung angeben, ohne new / delete zu verwenden.

Programmgröße: 15 408 Byte (verwendet 50% eines Maximums von 30 720 Byte) (2,60 Sekunden)
Minimale Speichernutzung: 1273 Byte (62% eines Maximums von 2048 Byte)

Der RAM-Verbrauch hat leicht zugenommen. Aber das ist in der Tat zum Besseren. Der Anstieg des RAM-Verbrauchs erklärt sich aus der Bewegung von Objekten vom Heap in einen statisch verteilten Speicherbereich. Das heißt,Tatsächlich wurden Objekte zuvor erstellt, dies ging jedoch nicht in die Statistik. Und jetzt werden diese Objekte explizit berücksichtigt.

Aber den Blitzverbrauch deutlich zu reduzieren, hat nicht funktioniert. Der Code enthält weiterhin die Konstruktoren selbst, die beim Start noch aufgerufen werden. Das heißt,Der Compiler konnte sie nicht im Voraus ausführen und alle Werte in vorab zugewiesene Bereiche einfügen. Und es gab immer noch Destruktoren im Code, obwohl dem Igel klar ist, dass Objekte niemals gelöscht werden.

Um zumindest ein wenig zu sparen, habe ich alle Destruktoren in der Hierarchie und insbesondere den virtuellen Destruktor in der Basisklasse gelöscht. Die Idee war, ein paar Bytes in jeder vtable freizugeben. Und dann erwartete mich eine Überraschung:

Programmgröße: 14 704 Bytes (verwendet 48% eines Maximums von 30 720 Bytes) (2,94 Sekunden)
Minimale Speichernutzung: 1211 Bytes (59% eines Maximums von 2048 Bytes)

Es stellte sich heraus, dass die vtable nicht um einen Zeiger ging, sondern bereits um 2. Außerdem hatten beide mit dem Destruktor zu tun. Nur ein Destruktor ist leer (anscheinend für Objekte auf dem Stapel) und der andere mit einem freien Aufruf, der für Objekte auf dem Heap sichtbar ist (-12 Byte RAM). Außerdem sind die mit der Hüfte verknüpften Variablen (8 Byte) und die Beschriftungen von Objekten, die nie erstellt wurden (Bildschirm, ParentScreen - 40 Byte), weg. Der

Flash-Verbrauch ist erheblich gesunken - um 700 Byte. Nicht nur die Destruktoren selbst, sondern auch die Implementierungen malloc / free / new / delete sind verschwunden. 700 Bytes für einen leeren virtuellen Destruktor! 700 Bytes, Carl!

Das würde nicht hin und her gehen, hier sind alle Zahlen an einem Ort

WarC.C ++
Flash15 45814.56814.704
RAM125811761211


Fazit: Der Verbrauch in C ++ war fast der gleiche wie in C. Gleichzeitig sind Kapselung, Vererbung und Polymorphismus Macht. Ich bin bereit, dies mit einem gewissen Anstieg des Verbrauchs zu überbezahlen. Vielleicht kann ich einfach nicht schön in C schreiben, aber warum, wenn ich schön in C ++ schreiben kann?

Nachwort


Anfangs wollte ich am Ende des Projekts einen Artikel schreiben. Da sich die Notizen jedoch im Verlauf der Arbeit mit hoher Geschwindigkeit ansammeln, droht der Artikel sehr groß zu sein. Also habe ich beschlossen, es in mehrere Teile zu teilen. In diesem Teil habe ich über die Vorbereitungsphasen gesprochen: Verstehen, was ich wirklich will, Auswahl einer Plattform, Implementierung eines Anwendungsframeworks.

Im nächsten Teil möchte ich mit der Implementierung der Grundfunktionalität fortfahren - mit GPS arbeiten. Ich habe bereits einige interessante Rechen gesehen, von denen ich erzählen möchte.

Seit mehr als 10 Jahren habe ich nicht ernsthaft für Mikrocontroller programmiert. Es stellte sich heraus, dass ich durch die Fülle an Ressourcen großer Computer etwas verwöhnt wurde und es in den Realitäten von ATMega32 eng ist. Daher musste ich über verschiedene Sicherungsoptionen nachdenken, z. B. das Trimmen der Funktionalität von Bibliotheken oder das Neugestalten der Anwendung im Namen einer effizienten Speichernutzung. Ich schließe auch die Möglichkeit nicht aus, auf leistungsstärkere Controller umzusteigen - ATMega64 oder etwas aus der STM32-Reihe.

Der Artikel entpuppt sich stilistisch als Bauzeitschrift. Und ich freue mich über konstruktive Kommentare - es ist noch nicht zu spät, etwas zu ändern. Wer möchte, kann sich meinem Projekt auf dem Github anschließen .

Das Ende des ersten Teils.

Zweiter Teil

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


All Articles