Singleton platziert Objekte im ROM und statische Variablen (C ++ am Beispiel des Cortex M4-Mikrocontrollers)

Bild

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() { //   Timer1      auto& objRef = Singleton<Timer1>::GetInstance(); //  ,      auto& objRef1 = Singleton<Timer1>::GetInstance(); return 0; } 

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 ; //        RAM constexpr GreenLed greenLed ; //        ROM int main() { static GreenLed myGreenLed ; //     RAM Led1 led1; //     myGreenLed.Toggle(); led1.Toggle() ; } 

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 { // Singleton   ,     friend class Singleton<Pin> ; public: __forceinline inline void Toggle() const { T::Toggle(pinNum) ; } //  = const Pin & operator=(const Pin &) = delete ; private: // ,      constexpr Pin() {} ; //  ,      //   ,      constexpr Pin(const Pin &) = default ; } ; 


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 Compileroptimierung
Modullokale 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() { //        GreenLed //   GreenLed& myGreenLed = Singleton<GreenLed>::GetInstance(); //            Led1& led1 = Singleton<Led1>::GetInstance(); myGreenLed.Toggle() ; led1.Toggle() ; //  , Singleton<Led1>::GetInstance().Toggle() } 

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.)

Bild

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 Absturz
Die 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: // constexpr  constexpr   //           T static constexpr T instance{T()}; } ; template<typename T> constexpr T Singleton<T>::instance ; 

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; //    RAM const C& test2; //    ROM //      constexpr A(const A &) = default ; //     RAM  ROM,  Singleton constexpr A() : test(Singleton<B>::GetInstance()), test2(Singleton<C>::GetInstance()) { } }; int main() { //      ROM auto& myObject = Singleton<A>::GetInstance() ; //           myObject.Set(myObject.Get()) ; cout<<"Singleton<A> - address: "<< &myObject <<std::endl; } 

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
 //    ,     ROM class RomObject{}; //  ROM  template<typename T> class Singleton<T, typename std::enable_if_t<std::is_base_of<RomObject, T>::value>> { public: Singleton(const Singleton&) = delete; Singleton& operator = (const Singleton&) = delete; Singleton() = delete; static constexpr const T& GetInstance() { return instance; } private: static constexpr T instance{T()}; }; template<typename T> constexpr T Singleton<T, typename std::enable_if_t<std::is_base_of<RomObject, T>::value>>::instance ; //  RAM  template<typename T, class Enable = void> class Singleton { public: Singleton(const Singleton&) = delete; Singleton& operator = (const Singleton&) = delete; Singleton() = delete; constexpr static T& GetInstance() { return instance; } private: static T instance ; }; template<typename T, class Enable> T Singleton<T,Enable>::instance ; 


In diesem Fall können Sie sie folgendermaßen verwenden:

 //      RAM,   SetB()    (j) class B { friend class Singleton<B>; public: const B & operator=(const B &) = delete ; void SetB(int value) { j = value ; } private: // ,        B(const B &) = default ; B() = default; int j = 0; } //      ROM class A: public RomObject{ friend class Singleton<A>; public: const A & operator=(const A &) = delete ; int Get() const { return test2.Get(); } //     B,    void Set(int v) const { test.SetB(v); } private: B& test; //    RAM const C& test2; //    ROM //        A(const A &) = default ; //     RAM  ROM,  Singleton constexpr A() : test(Singleton<B>::GetInstance()), test2(Singleton<C>::GetInstance()) { } }; int main() { //      ROM auto& romObject = Singleton<A>::GetInstance() ; //    B  RAM auto& ramObject = Singleton<B>::GetInstance() ; //           ramObject.SetB(romObject.Get()) ; cout<<"Singleton<A> - address: "<< &romObject <<std::endl; cout<<"Singleton<B> - address: "<< &ramObject <<std::endl; } 

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.

LED-Objekt
 //      class ITimerSubscriber { public: virtual void OnTimeOut() const = 0; } ; template <typename T, std::uint8_t pinNum> class Pin: public RomOject, public ITimerSubscriber { // Singleton   ,     friend class Singleton<Pin> ; public: __forceinline inline void Toggle() const { T::Toggle(pinNum) ; } //       __forceinline inline void OnTimeOut() const override { Toggle() ; } //  = const Pin & operator=(const Pin &) = delete ; private: // ,      constexpr Pin() = default ; Pin(const Pin &) = default ; } ; 

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 ; } //      __forceinline inline void OnInterrupt() { if ((timer.SR & TIM_SR_UIF) && (timer.DIER & TIM_DIER_UIE)) { //   ,     OnTimeOut //       Toggle() subscriber->OnTimeOut() ; timer.SR &=~ TIM_SR_UIF ; } } //    TimeOut  ,   ITimerSubscriber,   __forceinline inline void Subscribe(const ITimerSubscriber& obj) { subscriber = &obj ; } inline void Start() { timer.CR1 |= TIM_CR1_URS ; timer.DIER |= TIM_DIER_UIE ; SetPeriod(period) ; timer.CR1 &=~TIM_CR1_OPM ; timer.EGR |= TIM_EGR_UG ; timer.CR1 |= TIM_CR1_CEN ; } protected: // ,         explicit Timer(TIM_TypeDef& tim): timer{tim} {}; const ITimerSubscriber * subscriber = nullptr ; TIM_TypeDef& timer ; std::uint16_t period = 1000; } ; 


 //       class BlinkTimer: public Timer { friend class Singleton<BlinkTimer> ; public: const BlinkTimer & operator=(const BlinkTimer &) = delete ; private: BlinkTimer(const BlinkTimer &) = default ; inline BlinkTimer(): Timer{*TIM2} { } } ; int main() { BlinkTimer & blinker = Singleton<BlinkTimer>::GetInstance() ; using Led1 = Pin<PortA, 5> ; // Led1,   ROM,      blinker.Subscribe(Singleton<Led1>::GetInstance()) ; blinker.Start() ; } 

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() { //   BlinkTimer Singleton<BlinkTimer>::GetInstance().OnInterrupt(); } }; using tIntFunct = void(*)(); using tIntVectItem = union {tIntFunct __fun; void * __ptr;}; #pragma segment = "CSTACK" #pragma location = ".intvec" const tIntVectItem __vector_table[] = { { .__ptr = __sfe( "CSTACK" ) }, //    __iar_program_start, //      InterruptHandler::DummyHandler, InterruptHandler::DummyHandler, InterruptHandler::DummyHandler, InterruptHandler::DummyHandler, InterruptHandler::DummyHandler, 0, 0, 0, 0, InterruptHandler::DummyHandler, InterruptHandler::DummyHandler, 0, InterruptHandler::DummyHandler, InterruptHandler::DummyHandler, //External Interrupts InterruptHandler::DummyHandler, //Window Watchdog InterruptHandler::DummyHandler, //PVD through EXTI Line detect/EXTI16 .... InterruptHandler::Timer2Handler, //      BlinkTimer InterruptHandler::DummyHandler, //TIM3 ... InterruptHandler::DummyHandler, //SPI 5 global interrupt }; extern "C" void __cmain(void) ; extern "C" __weak void __iar_init_core(void) ; extern "C" __weak void __iar_init_vfp(void) ; #pragma required = __vector_table void __iar_program_start(void) { __iar_init_core() ; __iar_init_vfp() ; __cmain() ; } 

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.

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


All Articles