
In einem früheren Artikel,
Wo sind Ihre Konstanten auf einem CortexM-Mikrocontroller gespeichert (am Beispiel des C ++ IAR-Compilers) , wurde die Frage diskutiert, wie konstante Objekte im ROM platziert werden. Jetzt möchte ich Ihnen sagen, wie Sie das Einzelgeneratormuster verwenden können, um Objekte im ROM zu erstellen.
Einführung
Es wurde bereits viel über Singleton (im Folgenden als Singleton bezeichnet), seine positiven und negativen Seiten, geschrieben. Trotz seiner Mängel weist es viele nützliche Eigenschaften auf, insbesondere im Zusammenhang mit der Firmware für Mikrocontroller.
Für eine zuverlässige Mikrocontroller-Software wird zunächst nicht empfohlen, Objekte dynamisch zu erstellen, sodass sie nicht gelöscht werden müssen. Oft werden Objekte einmal erstellt und leben vom Start des Geräts bis zum Ausschalten. Ein solches Objekt kann sogar ein Port-Leg sein, an das eine LED angeschlossen ist, es wird einmal erstellt und es wird sicherlich nirgendwo hingehen, während die Anwendung ausgeführt wird, und es kann offensichtlich Singleton sein. Jemand sollte solche Objekte erstellen und es könnte Singleton sein.
Singleton gibt Ihnen auch die Garantie, dass dasselbe Objekt, das den Portabschnitt beschreibt, nicht zweimal erstellt wird, wenn es plötzlich an mehreren Stellen verwendet wird.
Eine weitere meiner Meinung nach bemerkenswerte Eigenschaft von Singleton ist die einfache Bedienung. Zum Beispiel, wie im Fall des Interrupt-Handlers, ein Beispiel, mit dem am Ende des Artikels steht. Aber im Moment werden wir uns selbst um Singleton kümmern.
Singleton erstellt Objekte im RAM
Im Allgemeinen wurden bereits ziemlich viele Artikel darüber geschrieben,
Singleton (Loner) oder eine statische Klasse? oder
Drei Zeitalter des Singleton-Musters . Daher werde ich mich nicht auf Singleton konzentrieren und all die vielen Optionen für seine Implementierung beschreiben. Stattdessen werde ich mich auf zwei Optionen konzentrieren, die in der Firmware verwendet werden können.
Zunächst werde ich klarstellen, was der Unterschied zwischen der Firmware für den Mikrocontroller und der üblichen ist und warum einige Singleton-Implementierungen für diese Software „besser“ sind als andere. Einige Kriterien ergeben sich aus den Anforderungen an die Firmware, andere nur aus meiner Erfahrung:
- In der Firmware wird nicht empfohlen, Objekte dynamisch zu erstellen
- In der Firmware wird ein Objekt häufig statisch erstellt und niemals zerstört.
- Nun, wenn der Ort des Objekts in der Kompilierungsphase bekannt ist
Basierend auf diesen Annahmen betrachten wir zwei Varianten von Singleton mit statisch erstellten Objekten, und die wahrscheinlich bekannteste und häufigste ist Meyers Singleton. Obwohl es nach dem C ++ - Standard threadsicher sein sollte, machen es Compiler für Firmware so (z. B. IAR). Nur wenn die Sonderoption aktiviert ist:
template <typename T> class Singleton { public: static T & GetInstance() { static T instance ; return instance ; } Singleton() = delete ; Singleton(const Singleton<T> &) = delete ; const Singleton<T> & operator=(const Singleton<T> &) = delete ; } ;
Es verwendet eine verzögerte Initialisierung, d.h. Die Initialisierung eines Objekts erfolgt nur beim ersten
GetInstance()
. Betrachten Sie dies als dynamische Initialisierung.
int main() {
Und Singleton ohne verzögerte Initialisierung:
template <typename T> class Singleton { public: static constexpr T & GetInstance() { return instance ; } Singleton() = delete ; Singleton(const Singleton<T> &) = delete ; const Singleton<T> & operator=(const Singleton<T> &) = delete ; private: inline static T instance ;
Beide Singleton erstellen Objekte im RAM. Der Unterschied besteht darin, dass die Initialisierung für die zweite unmittelbar nach dem Start des Programms erfolgt und die erste beim ersten Aufruf initialisiert wird.
Wie können sie im wirklichen Leben eingesetzt werden? Nach alter Tradition werde ich versuchen, dies am Beispiel einer LED zu zeigen. Angenommen, wir müssen ein Objekt der Klasse
Led1
, das eigentlich nur ein Alias der Klasse
Pin<PortA, 5>
:
using PortA = Port<GpioaBaseAddr> ; using Led1 = Pin<PortA, 5> ; using GreenLed = Pin<PortA, 5> ; Led1 myLed ;
Nur für den Fall, dass die Klassen Port und Pin ungefähr so aussehen constexpr std::uint32_t OdrAddrShift = 20U; template <std::uint32_t addr> struct Port { __forceinline inline static void Toggle(const std::uint8_t bit) { *reinterpret_cast<std::uint32_t*>(addr ) ^= (1 << bit) ; } }; template <typename T, std::uint8_t pinNum> class Pin {
Im Beispiel habe ich bis zu 4 verschiedene Objekte desselben Typs in RAM und ROM erstellt, die tatsächlich mit derselben Ausgabe von Port A arbeiten. Was hier nicht sehr gut ist:
Nun, das erste ist, dass ich anscheinend vergessen habe, dass
GreenLed
und
Led1
vom gleichen Typ sind, und mehrere identische Objekte erstellt habe, die an verschiedenen Adressen Platz
Led1
. Tatsächlich habe ich sogar vergessen, dass ich bereits globale Objekte der
GreenLed
Led1
und
GreenLed
erstellt und auch lokal erstellt habe.
Zweitens ist es im Allgemeinen nicht erwünscht, globale Objekte zu deklarieren.
Programmierrichtlinien für eine bessere CompileroptimierungModullokale Variablen - Variablen, die als statisch deklariert sind - werden bevorzugt
globale Variablen (nicht statisch). Vermeiden Sie es auch, die Adresse häufig aufgerufener statischer Variablen zu verwenden.
und lokale Objekte sind nur im Rahmen der Funktion main () verfügbar.
Daher schreiben wir dieses Beispiel mit Singleton neu:
using PortA = Port<GpioaBaseAddr> ; using Led1 = Pin<PortA, 5> ; using GreenLed = Pin<PortA, 5> ; int main() {
In diesem Fall verweisen meine Links, egal was ich vergesse, immer auf dasselbe Objekt. Und ich kann diesen Link überall im Programm erhalten, in jeder Methode, einschließlich zum Beispiel in der statischen Methode des Interrupt-Handlers, aber dazu später mehr. Fairerweise muss ich sagen, dass der Code nichts tut und der Fehler in der Programmlogik nicht verschwunden ist. Okay, lassen Sie uns herausfinden, wo und wie sich dieses von Singleton erstellte statische Objekt im Allgemeinen befand und wie es initialisiert wurde.
Statisches Objekt
Bevor Sie es herausfinden, wäre es schön zu verstehen, was ein statisches Objekt ist.
Wenn Sie Klassenmitglieder mit dem Schlüsselwort static deklarieren, bedeutet dies, dass Klassenmitglieder einfach nicht an Klasseninstanzen gebunden sind, sondern unabhängige Variablen sind und Sie auf solche Felder zugreifen können, ohne ein Klassenobjekt zu erstellen. Nichts bedroht ihr Leben vom Moment ihrer Geburt bis zur Veröffentlichung des Programms.
Bei Verwendung in einer Objektdeklaration bestimmt der statische Bezeichner nur die Lebensdauer des Objekts. Grob gesagt wird der Speicher für ein solches Objekt beim Start des Programms zugewiesen und beim Beenden des Programms freigegeben. Beim Start werden sie ebenfalls initialisiert. Ausnahmen sind nur lokale statische Objekte, die, obwohl sie erst am Ende des Programms "sterben", im Wesentlichen "geboren" sind oder vielmehr beim ersten Durchlaufen ihrer Deklaration initialisiert werden.
Die dynamische Initialisierung einer lokalen Variablen mit statischem Speicher wird zum ersten Mal zum Zeitpunkt des ersten Durchlaufs ihrer Deklaration durchgeführt. Eine solche Variable gilt nach Abschluss ihrer Initialisierung als initialisiert. Wenn ein Thread zum Zeitpunkt seiner Initialisierung durch einen anderen Thread eine Variablendeklaration durchläuft, muss er warten, bis die Initialisierung abgeschlossen ist.
In den folgenden Aufrufen erfolgt keine Initialisierung. All dies kann auf eine Phrase reduziert werden, es kann
nur eine Instanz eines statischen Objekts existieren.Solche Schwierigkeiten führen dazu, dass die Verwendung lokaler statischer Variablen und Objekte in der Firmware zu zusätzlichem Overhead führt. Sie können dies anhand eines einfachen Beispiels überprüfen:
struct Test1{ Test1(int value): j(value) {} int j; } ; Test1 &foo() { static Test1 test(10) ; return test; } int main() { for (int i = 0; i < 10; ++i) { foo().j ++; } return 0; }
Hier muss der Compiler beim ersten Aufruf der Funktion
foo()
überprüfen, ob das lokale statische Objekt
test1
noch nicht initialisiert wurde, und den Konstruktor des Objekts
Test1(10)
In der zweiten und den folgenden Durchgängen muss er sicherstellen, dass das Objekt bereits initialisiert ist, und diesen Schritt überspringen. direkt zum
return test
.
Zu diesem
foo()::static guard for test 0x00100004 0x1 Data Lc main.o
fügt der Compiler einfach ein zusätzliches Schutzflag
foo()::static guard for test 0x00100004 0x1 Data Lc main.o
und fügt den Bestätigungscode ein. Bei der ersten Deklaration einer statischen Variablen wird dieses Schutzflag nicht gesetzt, und daher muss das Objekt durch Aufrufen des Konstruktors initialisiert werden. Während des nächsten Durchlaufs ist dieses Flag bereits gesetzt, sodass keine Initialisierung mehr erforderlich ist und der Konstruktoraufruf übersprungen wird. Darüber hinaus wird diese Prüfung kontinuierlich in der for-Schleife durchgeführt.

Wenn Sie die Option aktivieren, die Ihnen die Initialisierung in Multithread-Anwendungen garantiert, wird noch mehr Code angezeigt ... (Der Aufruf zum Erfassen und Freigeben der Ressource während der Initialisierung ist orange unterstrichen.)

Somit steigt der Preis für die Verwendung einer statischen Variablen oder eines statischen Objekts in der Firmware sowohl in der RAM-Größe als auch in der Codegröße. Und diese Tatsache wäre schön zu berücksichtigen und bei der Entwicklung zu berücksichtigen.
Ein weiterer Nachteil ist die Tatsache, dass das Schutzflag zusammen mit der statischen Variablen erstellt wird, seine Lebensdauer der Lebensdauer des statischen Objekts entspricht, vom Compiler selbst erstellt wird und Sie während der Entwicklung keinen Zugriff darauf haben. Das heißt, wenn plötzlich aus irgendeinem Grund
siehe zufälliger AbsturzDie Ursachen für zufällige Fehler sind: (1) Alpha-Teilchen, die aus dem Zerfallsprozess resultieren, (2) Neutronen, (3) eine externe Quelle elektromagnetischer Strahlung und (4) internes Übersprechen.
Wenn das Flag von 1 auf 0 geht, wird die Initialisierung mit dem Anfangswert erneut aufgerufen. Das ist nicht gut und man muss auch bedenken. So fassen Sie die statischen Variablen zusammen:
Für jedes statische Objekt (sei es eine lokale Variable oder ein Klassenattribut) wird der Speicher einmal zugewiesen und ändert sich nicht in der gesamten Anwendung.
Lokale statische Variablen werden beim ersten Durchlauf durch eine Variablendeklaration initialisiert.
Statische Klassenattribute sowie statische globale Variablen werden unmittelbar nach dem Start der Anwendung initialisiert. Darüber hinaus ist diese Reihenfolge nicht definiert
Nun zurück zu Singleton.
Singleton platziert Objekt im ROM
Aus all dem können wir schließen, dass Singleton Mayers für uns die folgenden Nachteile haben kann: zusätzliche RAM- und ROM-Kosten, ein unkontrolliertes Sicherheitsflag und die Unfähigkeit, ein Objekt aufgrund dynamischer Initialisierung im ROM zu platzieren.
Aber er hat ein wunderbares Plus: Sie steuern die Initialisierungszeit des Objekts. Nur der Entwickler selbst ruft
GetInstance()
ersten Mal auf, wenn er es benötigt.
Um die ersten drei Mängel zu beseitigen, reicht es aus, sie zu verwenden
Singleton ohne verzögerte Initialisierung template<typename T, class Enable = void> class Singleton { public: Singleton(const Singleton&) = delete ; Singleton& operator = (const Singleton&) = delete ; Singleton() = delete ; static T& GetInstance() { return instance; } private: static T instance ; } ; template<typename T, class Enable> T Singleton<T,Enable>::instance ;
Hier gibt es natürlich ein anderes Problem: Wir können die Initialisierungszeit des Instanzobjekts nicht steuern und müssen irgendwie eine sehr transparente Initialisierung bereitstellen. Dies ist jedoch ein separates Problem, auf das wir jetzt nicht näher eingehen werden.
Dieser Singleton kann so umgestaltet werden, dass die Initialisierung des Objekts zur Kompilierungszeit vollständig statisch ist und eine Instanz von
T
im ROM unter Verwendung einer
static constexpr T instance
anstelle einer
static T instance
:
template <typename T> class Singleton { public: static constexpr T & GetInstance() { return instance ; } Singleton() = delete ; Singleton(const Singleton<T> &) = delete ; const Singleton<T> & operator=(const Singleton<T> &) = delete ; private:
Hier wird die Erstellung und Initialisierung des Objekts vom Compiler in der Kompilierungsphase durchgeführt und das Objekt fällt in das Segment .readonly. Die Klasse selbst muss die folgenden Regeln erfüllen:
- Die Initialisierung eines Objekts dieser Klasse muss statisch sein. (Der Konstruktor muss constexpr sein)
- Die Klasse muss über einen constexpr-Kopierkonstruktor verfügen
- Klassenmethoden eines Klassenobjekts sollten die Daten eines Klassenobjekts nicht ändern (alle const-Methoden)
Zum Beispiel ist diese Option durchaus möglich:
class A { friend class Singleton<A>; public: const A & operator=(const A &) = delete ; int Get() const { return test2.Get(); } void Set(int v) const { test.SetB(v); } private: B& test;
Großartig, Sie können Singleton verwenden, um Objekte im ROM zu erstellen, aber was ist, wenn sich einige Objekte im RAM befinden sollten? Natürlich müssen Sie zwei Spezialisierungen für Singleton beibehalten, eine für RAM-Objekte und eine für Objekte im ROM. Sie können dies tun, indem Sie beispielsweise für alle Objekte eingeben, die in die ROM-Basisklasse eingefügt werden sollen:
Spezialisierung für Singleton beim Erstellen von Objekten in ROM und RAM In diesem Fall können Sie sie folgendermaßen verwenden:
Wie können Sie einen solchen Singleton im wirklichen Leben verwenden?
Singleton Beispiel
Ich werde versuchen, dies am Beispiel der Funktionsweise des Timers und der LED zu zeigen. Die Aufgabe ist einfach, blinken Sie die LED am Timer. Der Timer kann eingestellt werden.
Das Funktionsprinzip lautet wie folgt: Wenn der Interrupt aufgerufen wird, wird die
OnInterrupt()
-Methode des Timers aufgerufen, die wiederum die LED-Schaltmethode über die Teilnehmerschnittstelle aufruft.
Offensichtlich muss sich das LED-Objekt im ROM befinden, da es keinen Sinn macht, es im RAM zu erstellen, es sind nicht einmal Daten darin. Im Prinzip habe ich es bereits oben beschrieben.
RomObject
also einfach die Vererbung von
RomObject
einen constexpr-Konstruktor und erben Sie auch die Schnittstelle zum Verarbeiten von Ereignissen vom Timer.
Aber ich werde den Timer mit einem kleinen Frachtbrief speziell im RAM
TIM_TypeDef
, einen Link zur
TIM_TypeDef
Struktur, einen Punkt und einen Abonnentenlink speichern und den Timer im Konstruktor konfigurieren (obwohl es möglich wäre, den Timer auch in den ROM zu verschieben):
Klassen-Timer class Timer { public: const Timer & operator=(const Timer &) = delete ; void SetPeriod(const std::uint16_t value) { period = value ; timer.PSC = TimerClockSpeed / 1000U - 1U ; timer.ARR = value ; }
In diesem Beispiel befand sich ein Objekt der Klasse
BlinkTimer
im RAM und ein Objekt der Klasse
Led1
im ROM. Keine zusätzlichen globalen Objekte im Code. An der Stelle, an der die Klasseninstanz benötigt wird, rufen wir einfach
GetInstance()
für diese Klasse auf
Es bleibt noch ein Interrupt-Handler zur Interrupt-Vektortabelle hinzuzufügen. Und hier ist es sehr bequem, Singleton zu verwenden. In der statischen Methode der Klasse, die für die Behandlung von Interrupts verantwortlich ist, können Sie die Methode des in Singleton eingeschlossenen Objekts aufrufen.
extern "C" void __iar_program_start(void) ; class InterruptHandler { public: static void DummyHandler() { for(;;) {} } static void Timer2Handler() {
Ein wenig über den Tisch selbst, wie alles funktioniert:Unmittelbar nach dem Einschalten oder nach einem Zurücksetzen wird ein Zurücksetzen mit der Zahl -8 unterbrochen. In der Tabelle handelt es sich um ein Nullelement. Gemäß dem Rücksetzsignal wechselt das Programm zum Nullelementvektor, wobei der Zeiger auf die Oberseite des Stapels zuerst initialisiert wird. Diese Adresse wird vom Speicherort des STACK-Segments übernommen, das Sie in den Linker-Einstellungen konfiguriert haben. Gehen Sie unmittelbar nach der Initialisierung des Zeigers zum Programmeintrittspunkt, in diesem Fall unter der Adresse der Funktion __iar_program_start
. Als Nächstes wird der Code initialisiert, um Ihre globalen und statischen Variablen zu initialisieren, den Coprozessor mit einem Gleitkomma zu initialisieren, sofern er in den Einstellungen enthalten war, und so weiter. Wenn ein Interrupt auftritt, geht der Interrupt-Controller anhand der Interrupt-Nummer in der Tabelle an die Adresse des Interrupt-Handlers. In unserem Fall ist dies InterruptHandler::Timer2Handler
, der über Singleton die OnInterrupt()
-Methode unseres Blink-Timers OnTimeOut()
, die wiederum die OnTimeOut()
-Methode des OnTimeOut()
.
Eigentlich ist das alles, Sie können das Programm ausführen. Ein Arbeitsbeispiel für IAR 8.40
liegt hier .
Ein detaillierteres Beispiel für die Verwendung von Singleton für Objekte in ROM und RAM finden
Sie hier .
Dokumentationslinks:
PS Auf dem Bild am Anfang des Artikels ist Singleton trotzdem kein ROM, sondern WHISKY.